Painlessly managing long running processes in your views using Ruby on Rails
There is currently no convenient or “natural” way to transfer information directly from the backend to your views in Ruby on Rails. This is particularly inconvenient when trying to transfer information about the progress of a long running process within the backend (such as the processing of an image). It is possible to simply use JavaScript to make XHR/fetch requests every couple of seconds/minutes to request updated information, but it is more of a workaround than an actual solution.
This problem has been explored by other developers (such as Tomasz Gryszkiewicz in his work on Drab for Elixir) and the general consensus seems to be to use WebSockets or SSEs to easily and reliably relay information to the frontend from the backend in real time.
While Ruby on Rails does provide a framework for WebSockets called Action Cable, it does not provide a convenient way to make frontend modifications once the state of a process changes (which would require to write a new collection of frontend and backend code). Additionally, some work would also need to go into creating and managing individual channels for each user in order to relay the information back to them. Overall, this process is lengthy and needs to be standardized.
To solve these problems, there exists a frontend framework for Ruby on Rails called fie which synchronizes the state of your application between your views and backend over a permanent WebSocket connection. If you have not yet used fie you can install it in your application using the “Quick Start” and can then follow a simple tutorial that I posted previously which is titled “Making a Slideshow using Ruby on Rails with no JavaScript”.
Let’s delve into how to simulate and manage long running processes in fie.
You probably already know about commanders, which are used by fie in the same way controllers are used by Ruby on Rails. However, since long running processes are handled by the Active Jobs framework which is external to fie’s commanders, fie has another feature calledManipulator
which injects your classes with the ability to manipulate the state of your user’s views in the same way as the commander.
By including Manipulator
within your classes, you provide them with access to basic fie commander methods which include state
for viewing and changing the shared state, execute_js_function
to execute a JavaScript function from the backend, and commander_exists?
to verify whether the commander/client whose state you wish to manipulate still exists.
Your first step to recreating the example displayed in the GIF above is to define your instance variables (or your overall state) within your controller which should look like this:
As you can see, two instance variables are created and will be used for the following purpose:
@long_job_progress
keeps track of the progress of our simulated long running job, which is 0 out of 100 at the time of initialization.@is_long_job_running
, verifies whether the long running job is currently running or not. If we do not keep track of whether the job is already running or not, we risk having the same job run multiple times concurrently, especially if the view does not prevent the user from triggering it multiple times for one reason or another.
Once your state is initialized in your controller, the next step is to create your view which will display the progress of your long running job to the user:
The view may display one of two things depending on whether the job is already running or not (which is represented in the @is_long_job_running
instance variable).
- If the job is already running, the user will see a progress bar equal to the current value of the
@long_job_progress
instance variable and a maximum value of 100. Therefore as the value of@long_job_progress
changes, so will the progress bar. - If the job has finished or not yet started, the user will simply see a button that will prompt them to start the long running job. If the user clicks on the button, the commander method
start_long_job
will be triggered.
As the button is currently triggering a method named start_long_job
in our commander and it does not yet exist, we will create it. It should look like this:
To begin with, if the job is currently running, the method simply exits (through an empty return). Otherwise, the value of @long_job_progress
is reset to 0 and the long job is set as running through modifying the value of @is_long_job_running
to true. The actual job is then set to run asynchronously through its perform_later
method. We also pass the identifier of our commander @connection_uuid
to the job so that the commander can manipulate the state of our commander/view.
Let’s now create the part we have been waiting for, the actual JOB (in the app/jobs folder):
It is first important to note that we include the Manipulator
module through include Fie::Manipulator
in order to have access to the methods necessary to modify the state from our job.
Once we include Fie::Manipulator
, the next step is to define the perform method of our job which accepts as an argument the identifier connection_uuid
of our commander/view. In order for the methods included from Fie::Manipulator
to work, we need to define the instance variable @fie_connection_uuid
as equal to theconnection_uuid
of the commander/view whose state we would like to manipulate. After doing so, we can create a lambda literal named is_job_finished
which will tell us whether the progress of the job is over 100 or not.
Based on whether the job has finished running or not (meaning the client disconnected or the job completed at a progress of ≥ 100), the job will complete one of two actions.
- If the client is still connected (verified through the
commander_exists?
method) and the job is still running (verified through callingis_job_running
), the value of@long_process_progress
is increased by 10, and after a waiting period of 1 second, the job is then run again. The user will see it as their progress bar being more complete. - Otherwise, the value of
@is_long_job_running
is set to false which will notify all other parts of the application that the job has finished running. The user will see this as their progress bar converting into a button again.
You can try this example at https://fie.eranpeer.co/showcase#long-running-job.
Visit the project pages at: https://fie.eranpeer.co or https://github.com/raen79/fie
Contact me or ask questions at: eran.peer79@gmail.com or https://gitter.im/rails-fie