Using AshStateMachine and AshOban

Here is my ash_state_machine :


  # start -> send_next_question -> send_results -> stop

  state_machine do
    initial_states([:start])
    default_initial_state(:start)

    transitions do
      transition(:begin_quiz, from: :start, to: :send_next_question)
      transition(:continue_quiz, from: :send_next_question, to: :send_next_question)
      transition(:process_results, from: :send_next_question, to: :send_results)
      transition(:error, from: [:start, :send_next_question, :send_results], to: :stop)
    end
  end


  attributes do
    uuid_primary_key :id

    # default id should be there.
    attribute :quiz_id, :uuid, allow_nil?: false
    attribute :user_id, :uuid, allow_nil?: false

    attribute :quiz, :map,  allow_nil?: true
    attribute :user, :map, allow_nil?: true

    # reponse for the question
    attribute :current_response, :string, allow_nil?: true
    attribute :previous_question, :uuid, allow_nil?: true
    attribute :next_question, :uuid, allow_nil?: true

    # show errors
    attribute :error, :string
    attribute :error_state, :string
    # :state attribute is added for you by `state_machine`
    
  end

Here is how I think it should work:

  1. since there is a state send_next_question which will transition to itself (with delay of 1s) until all the quiz.questions becomes empty list.

  2. when quiz.questions becomes empty, send_next_question goes to send_results.

  3. ash_oban triggers a job each time send_next_question state is achieved.

  4. ash_oban triggers a job when send_results state is achieved.

a high level idea of implementing it wouldbe helpful. :slight_smile:

1 Like

I think AshOban will not be the best case for this. I think you will want a GenServer handling the state of the resource, and transitioning from state to state. When a quiz is started (or perhaps resumed on reconnection) you can load up the relevant state machine and begin interacting with it. Generally speaking, less than one minute feedback loops aren’t the best fit for Oban/AshOban in my experience.

AshOban triggers can be triggered in one(or both) of two ways:

trigger :send_next_question do
  scheduler_cron "* * * * * *" # trigger for any matches every one minute
  where expr(state == :send_next_question)
end

...

# schedule the next invocation automatically
update :send_next_question do
  change run_oban_trigger(:send_next_question)
  # or in a custom change `AshOban.run_trugger`
end

For that latter case, we don’t have a run_trigger_later, but we could add one or you could hand roll one pretty easily. When you combine both of these, typically the scheduler_cron and where act as a backup in case something goes wrong triggering the actions. You can also only use the second strategy, and pass scheduler_cron nil to not automatically check for matches and trigger the action.

However, this is going to amount to a lot of database back and forth during what looks to be intended to be a pretty tight loop. Your user would likely have a better experience if the quiz flow was designed as a GenServer I think. I can see a case for having tooling for this kind of thing built into ash_state_machine, i.e some state machines should be statable, keep track of some in memory state, and have an accompanying module that handles state transitions in memory or something. Maybe :slight_smile:

1 Like

I agree on the GenServer part since wait time is less than a minute.
Nonetheless, this gives a good idea on how ash_oban and ash_state_machine work together.

Will manage the genserver for now :slight_smile:

2 Likes