state_machine using after_failure with ActiveRecord transactions - ruby

I'm using ruby state_machine to manage processing documents. As the document progresses the SM states manage the extra validations needed.
If any step fails, such as the document being the wrong dimensions, I want to record this so I can inform a user who is waiting for the conversion to complete.
My plan was to use SM after_failure to fill in a failed_at timestamp in the model. The polling user interface would see the processing has failed and display an appropriate message.
My issue is that if a transition fails it rolls back the transaction including the after_failure method. This means the user never finds out that the processing failed and unfortunately sits in front of their screen indefinitely.
In the example below I want the :fail_transaction method to run despite the transition failing. I'm open to any ideas or alternate ways to notify the user of failure.
Notes:
The :use_transaction => false option for state machine doesn't seem
to make a difference.
I considered putting the fail conversion into a worker queue to escape the transaction but as I'm using delayed_job so queue entries are also wound back.
This project is shamefully still on Rails 2.3.x & ruby 1.8.x
Example code:
state_machine :state, :initial => :new do
state :new
state :original_uploaded do
validates_presence_of :original_file
end
state :original_converted do
validates_presence_of :pdf
end
state :conversion_complete do
validates :pdf_dimensions_correct
end
event :process do
transition :new => :original_uploaded
transition :original_uploaded => :original_converted
transition :original_converted => :conversion_complete
end
after_transition :on => :process, :do => :process
before_transition any => :original_converted, :do => :convert_original_to_pdf
before_transition any => :conversion_complete, :do => :extract_pdf_metadata
after_failure :on => :process, :do => :fail_conversion
end
def fail_conversion
update_attribute(:failed_at, Time.now) # Should update dispite being invalid state
end
def convert_original_to_pdf
#...
end
def extract_pdf_metadata
#...
end
def pdf_dimensions_correct
errors.add(...)
end
Thanks in advance!

Related

Why is my AASM state machine not triggered with Rails 7 Turbo patch links?

I just updated my Rails 6 app to Rails 7 and have problems updating my :patch and :delete links to Turbo.
For example, in one of my views I have this link...
link_to("Mark as sent", status_url(quote), :data => {:'turbo_method' => :patch})
... which is handled by this controller:
class StatusController < ApplicationController
def update
#quote = Quote.find(params[:id])
#quote.send_it! # Should trigger AASM
flash[:notice] = "Quote marked as sent."
redirect_to edit_quote_path(#quote)
end
end
In the model I am using AASM as a state machine:
class Quote < ApplicationRecord
include AASM
aasm :column => "status" do
state :draft, :initial => true
state :inquired
state :sent
state :downloaded
state :accepted
state :rejected
event :send_it do
transitions :from => [:draft, :inquired], :to => :sent
end
...
event :reset_it do
transitions :from => [:inquired, :sent, :downloaded, :accepted, :rejected], :to => :draft
end
end
end
The problem is that the state machine does not get triggered when I hit the link. The flash message and the redirect work but the state is not changed in the database. When I replace #quote.send_it! with #quote.update_column(:status, "sent")it works, however.
Can anybody tell me what I'm missing here?
I don't quite see how turbo is related. Except that I think your redirect isn't actually working:
Redirected to http://127.0.0.1:3000/quotes/1/edit
Completed 302 Found in 18ms (ActiveRecord: 4.3ms | Allocations: 7265)
Started PATCH "/quotes/1/edit" for 127.0.0.1 at 2022-08-12
ActionController::RoutingError (No route matches [PATCH] "/quotes/1/edit"):
# NOTE: ^ not quite a redirect
# v but it doesn't show on a page, it just refreshes the current one.
Started GET "/quotes" for 127.0.0.1 at 2022-08-12 17:51:28 -0400
# and if the current page were /quotes/1/edit then it would look like
# redirect worked, but I was submitting from /quotes.
Update your controller to actually show any errors:
def update
#quote = Quote.find(params[:id])
# NOTE: if transition fails, `send_it!` returns `false`
# (or raises an error for invalid transitions)
# when you run `#quote.update_column(:status, "sent")`
# validations and state machine are not triggered and it works.
if #quote.send_it!
flash.notice = "Quote marked as sent."
else
flash.notice = #quote.errors.full_messages.join(", ")
end
respond_to do |format|
# in case you want add a stream response later
# format.turbo_stream { # TODO }
format.html { redirect_to edit_quote_path(#quote), status: :see_other }
# NOTE: Redirect as a GET request instead of PATCH ^
end
end
Or just add whiny_persistence flag and check the logs, this will raise validation errors:
aasm column: :status, whiny_persistence: true do
Not sure where you got the mark_as_ from, change that to #quote.aasm.fire! status.
Edit
Sorry, not status, needs to be the event, just use the right event.

cramp framework sync 'render' correct way using em-synchrony

To describe my problem I attach simple Cramp http://cramp.in/ class.
I add some modification but its mainly work like https://github.com/lifo/cramp-pub-sub-chat-demo/blob/master/app/actions/chat_action.rb
class ChatAction < Cramp::Websocket
use_fiber_pool
on_start :create_redis
on_finish :handle_leave, :destroy_redis
on_data :received_data
def create_redis
#redis = EM::Hiredis.connect('redis://127.0.0.1:6379/0')
end
def destroy_redis
#redis.pubsub.close_connection
#redis.close_connection
end
def received_data(data)
msg = parse_json(data)
case msg[:action]
when 'join'
handle_join(msg)
when 'message'
handle_message(msg)
else
# skip
end
end
def handle_join(msg)
#user = msg[:user]
subscribe
publish(:action => 'control', :user => #user, :message => 'joined the chat room')
end
def handle_leave
publish :action => 'control', :user => #user, :message => 'left the chat room'
end
def handle_message(msg)
publish(msg.merge(:user => #user))
# added only for inline sync tests
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
private
def subscribe
#redis.pubsub.subscribe('chat') do |message|
render(message)
end
end
def publish(message)
#redis.publish('chat', encode_json(message))
end
def encode_json(obj)
Yajl::Encoder.encode(obj)
end
def parse_json(str)
Yajl::Parser.parse(str, :symbolize_keys => true)
end
def render_json(hash)
render encode_json(hash)
end
end
More about what i try to do is in handle_message method.
I try send messages to client in correct order. First publish message to all subscribers, second render some internal info only for current connected client.
For above code client receives:
{"action":"message","user":"user1","message":"this info should appear after published message"}
{"action":"message","message":"simple message","user":"user1"}
Its not synchronized, because of em-hiredis defferable responses, probably.
So I try to synchronized it this way:
def handle_message(msg)
EM::Synchrony.sync publish(msg.merge(:user => #user))
EM::Synchrony.next_tick do # if I comment this block messages order is still incorrect
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
end
Now, client handle messages with correct order.
{"action":"message","message":"simple message","user":"user1"}
{"action":"message","user":"user1","message":"this info should appear after published message"}
My questions are:
When I comment EM::Synchrony.next_tick block, messages order is still incorrect. What meaning have EM::Synchrony.next_tick block in this example?
Is this good way to handle inline sync with Cramp or EventMachine ?
Is there a better, clearer way to handle it ?
Thank you!
I found solution of this problem, em-synchrony should work inline out of the box by requiring this library:
require 'em-synchrony/em-hiredis'
class ChatAction < Cramp::Websocket
Using EM::Synchrony.next_tick block is bad idea, with big help of em-synchrony community I add em-hiredis 0.2.1 compatibility patch on github
So now handle_message method looks like this:
def handle_message(msg)
publish(msg.merge(:user => #user))
render_json(:action => 'message', :user => #user, :message => "this info should appear after published message")
end
Don`t forget to take this gem from github
gem 'em-synchrony', :git=> 'git://github.com/igrigorik/em-synchrony.git'
Hope it helps someone.

How can I cause the state_machine block to use the default accessor instead of my custom accessor?

I'm implementing pluginaweek's state_machine gem. I narrowed the code down to just this so I can understand the problem more easily. Let's assume I only have one status for now:
class Event < ActiveRecord::Base
state_machine :status, :initial => :active_offer do
end
def status
'Active Offer'
end
end
The error I'm getting when creating new Event objects either via seeding or via the browser is {:status=>["is invalid"]}.
The plan is to include a condition for all the different statuses and return a custom string to the view. The entire project's views currently use the .status syntax so I'm trying to smoothly install this. I started this solution based on reading the api docs:
http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Overwriting+default+accessors
This is my main goal:
def status
if read_attribute(:status) == 'active_offer' && self.start_date > Time.now
'Active Offer'
elsif read_attribute(:status) == 'active_offer' && self.start_date < Time.now
'Expired'
else read_attribute(:status) == 'cancelled'
'Cancelled'
end
end
What can I do to make the state_machine block use the normal accessor so it gets the database value?
THE SILVER BULLET SOLUTION:
My main problem was the accessor override status. When state_machine code was running, it was reading the current status through my accessor override and therefore getting a custom string returned, which was an invalid status. I had to make state_machine use :state. I didn't do this originally because I already had a :state attr for provinces and such, so I migrated that to :address_state.
Pretty sure you just need to change the definition's name:
class Event < ActiveRecord::Base
state_machine :status, :initial => :active_offer do
end
def active_offer
'Active Offer'
end
end
EDIT:
If I understand you correctly, which I'm not certain I do, this will work:
state_machine :status, initial: :active_offer do
event :expire do
transition all => :expired
end
event :cancel do
transition all => :cancelled
end
end
Then you can do your if... statements, etc., in the controller or something, and transition them with something like #event.expire or if #event.expired. If you want it automated you'll need something like whenever.

triggering intermediary transition between states in ruby state_machine

i am using https://github.com/pluginaweek/state_machine
my code is
event :set_running do
transition any => :runnning
end
event :restart do
transition :failed => :restarting
end
after_transition :failed => :restarting do |job,transition|
job.set_running
end
after_transition :restarting => :running do |job,transition|
job.restart_servers
=begin
this takes some time. and i would like job state to be "restarting" while
it's restarting servers. but it doesn't happen (i suppose because of transaction)
until after_transition :failed => :restarting callback is finished.
so it actually doesnt happen at all because this callback triggers => :running transition
=end
end
in other words i would like to run "restart" event once and trigger intermediary transition while it's transferring from :failed to :running.
can i do that somehow using state_machine?
http://rdoc.info/github/pluginaweek/state_machine/master/StateMachine/Integrations/ActiveRecord
It is now possible to disable transactions:
state_machine :initial => :parked, :use_transactions => false do

Dynamic State Machine in Ruby? Do State Machines Have to be Classes?

Question is, are state machines always defined statically (on classes)? Or is there a way for me to have it so each instance of the class with has it's own set of states?
I'm checking out Stonepath for implementing a Task Engine. I don't really see the distinction between "states" and "tasks" in there, so I'm thinking I could just map a Task directly to a state. This would allow me to be able to define task-lists (or workflows) dynamically, without having to do things like:
aasm_event :evaluate do
transitions :to => :in_evaluation, :from => :pending
end
aasm_event :accept do
transitions :to => :accepted, :from => :pending
end
aasm_event :reject do
transitions :to => :rejected, :from => :pending
end
Instead, a WorkItem (the main workflow/task manager model), would just have many tasks. Then the tasks would work like states, so I could do something like this:
aasm_initial_state :initial
tasks.each do |task|
aasm_state task.name.to_sym
end
previous_state = nil
tasks.each do |tasks|
aasm_event task.name.to_sym do
transitions :to => "#{task.name}_phase".to_sym, :from => previous_state ? "#{task.name}_phase" : "initial"
end
previous_state = state
end
However, I can't do that with the aasm gem because those methods (aasm_state and aasm_event) are class methods, so every instance of the class with that state machine has the same states. I want it so a "WorkItem" or "TaskList" dynmically creates a sequence of states and transitions based on the tasks it has.
This would allow me to dynamically define workflows and just have states map to tasks.
Are state machines ever used like this? It seems that this ruby workflow gem is similar to what I'm describing.
Update: I can see doing something like the following, but it seems sort of hackish:
#implementation_state_machine = Class::new do
include AASM
aasm_initial_state :initial
tasks.each { |state| aasm_state :"#{task.name}"}
# ...
end
... where a property on my model would be implementation_state_machine. I'd have to override method_missing to delegate state-related methods (accepted_phase?) to the implementation anonymous class.
Yeah, that does seem very hacky and quite messy. I wrote a new gem recently that allows you to use dynamic 'to' transitions with a decision setting.
So instead of building your events and transitions dynamically, would be it be possible to map them out first, and use the decide setting to allow the transition decide which new state to enter? You can also wrap your from transition in an array so you wouldn't need to do :from => previous_state ? "#{task.name}_phase" : "initial", you could just do :from => [ :cool_task_phase, :initial ]
I find that setting out your transitions and events out first, allows you to get a greater picture on what your model is doing.
Check it out at http://github.com/ryanza/stateflow
Hopefully you can find some use out of this.
In my implementation state machine is a hash https://github.com/mpapis/state_attr
state_attr :state, {
nil => :first,
:first => [:second, :third],
:second => :last,
:third => nil,
}
you can define as many state attributes as you like
BTW: in the background there is still a class but only as a proxy to attribute

Resources