Ruby Tip: How to Finish a Method After an Interrupt, Before Exiting | by Nick Francisci | Apr, 2022

Photograph by Thomas Bormans on Unsplash

Congratulations! You’ve been employed on the up-and-coming startup Scorching Muffins for Scorching Takes. Your small business is to seek out the very best new tweets and ship the writer pancakes. Don’t fear about income: we’ll determine that out later.

Diagram showing a tweet, “Pencils are just pens working on their self confidence” followed by a picture of pancakes
The bulletproof enterprise mannequin of your startup. Pancake photo by Ash from Pexels

Your small business relies upon upon the truth that your muffins arrive freshly made. No person likes chilly pancakes. Chilly pancakes are the important thing downside you had been employed to resolve. Enable me to clarify.

Scorching Muffins gainfully employs tweet-finding English-majors who submit the tweets to an online app. Nonetheless, the hard-working community of pancake cooks depend on a venerable workstation program named Po’s Pancake Social gathering (PPP). Po’s software program accepts new orders by means of an area community API after which distributes the orders to your cooks by means of a mix of telnet and service pigeons.

In some way it’s a must to get orders from the web to PPP, however you’re fairly certain nothing good will come from forwarding public web visitors by means of your firewall to a 15-year-old piece of proprietary software program. So that you give you a distinct method.

Since you don’t need to expose PPP to the web, you want an middleman to question one thing on-line for brand new orders after which insert them into PPP. You can question the web-app, however then it’s a must to determine how your web-app will hold monitor of which orders are entered into PPP.

As a substitute, you arrange the web-app to make use of an internet queuing service like Amazon SQS. You’ll question the queue through a repeatedly looping Ruby script operating on PPP’s workstation. The script does the next.

  1. Get Request: Question for a brand new order request within the on-line queue. When the queuing service responds, it additionally hides the request from different queries (eg. from different copies of your script) for a minute. SQS calls this a visibility timeout.
  2. Create Order: Upon receiving a message, create order in PPP through its native community API
  3. Acknowledge Request: Mark the request within the queue as processed in order that it received’t be reprocessed when the visibility timeout expires.

General, this implementation could emulate one thing like this:

❯ ruby naive.rb      
[INFO 2022-04-04 20:53:02 -0400] #--- Beginning Employee Program ---#
[INFO 2022-04-04 20:53:02 -0400] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-04-04 20:53:07 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 20:53:08 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 20:53:09 -0400] Operation, iteration 0, full.
[INFO 2022-04-04 20:53:09 -0400] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
...

Simple.

Wait. What if step 2, ‘create order’, succeeds however the script crashes or exits earlier than step 3, ‘acknowledge’? We’d have created order however left the request on the requests queue.

We’d reprocess the message once we begin the script again up. We’d create not less than two orders.

Okay, effectively what if we modify the order of our operations? We may mark the message as processed as quickly as we obtain it and then create an order in PPP.

Nonetheless, if the crash happens after we acknowledge the request however earlier than we create the order, no order will ever be created. Somebody received’t get their pancakes!

You determine it’s much less dangerous to ship two orders of pancakes than none in any respect and stick to the unique order of operations. However a number of pancake orders per tweet isn’t excellent. This duplicate-pancake challenge will happen if the script a) crashes or b) exits throughout this ‘crucial part’ of the code.

Right here’s the excellent news: all of the actors concerned on this downside (the Scorching Muffins net app, SQS, and Po’s Pancake Social gathering) are fairly mature packages which can be unlikely to have breaking modifications or bugs that have an effect on your script. So long as you may get the script working within the first place, it’s going to most likely proceed to work. Script crashes are unlikely.

Script exits are very probably. They’ll happen each time the workstation restarts or somebody tries to stop your ruby script. Nonetheless, script exits are additionally in your management! Earlier than exiting, a well-behaved working system will ship you a message and provides you a chance to wash up. So, not less than for regular exits, you ought to have the ability to end processing the crucial part earlier than exiting this system.

A ‘regular’ exit right here means one through which the working system offers this system a possibility for a sleek exit. In UNIX bash these conditions can be one the place a program is distributed SIGINT, SIGTERM, or SIGQUIT however not SIGKILL or SIGSTOP. In case you’re a Home windows consumer, this is able to be the distinction between if you shut a program (a sleek exit) and if you get the Program Not Responding dialog and click on “Finish Now”.

If we exit our program partway by means of, it seems like this:

❯ ruby naive.rb      
[INFO 2022-04-04 20:53:02 -0400] #--- Beginning Employee Program ---#
[INFO 2022-04-04 20:53:02 -0400] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-04-04 20:53:07 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 20:53:08 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 20:53:09 -0400] Operation, iteration 0, full.
[INFO 2022-04-04 20:53:09 -0400] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
^Cnaive.rb:5:in `sleep': Interrupt
from naive.rb:5:in `operation'
from naive.rb:29:in `<essential>'

When Ruby receives an exit system from the working system, it throws a SignalException. The primary thread jumps to deal with that exception. You may rescue it by wrapping the block that’s executing when the interrupt is thrown.

Even so, you’ve interrupted your methodology execution although and execution strikes to your rescue block. This doesn’t remedy our downside!

❯ ruby signal_trap.rb 
[INFO 2022-04-04 20:49:22 -0400] #--- Beginning Employee Program ---#
[INFO 2022-04-04 20:49:22 -0400] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-04-04 20:49:27 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 20:49:28 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 20:49:29 -0400] Operation, iteration 0, full.
[INFO 2022-04-04 20:49:29 -0400] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
^C[WARN 2022-04-04 20:49:33 -0400] #--- Acquired interrupt ---#

An alternative choice is so as to add an at_exit block. The code on this block will execute when this system is exiting in response to the interrupt.

This method is a bit more compact however has the identical challenge as a sign lure: interrupting this system will nonetheless interrupt the execution of our code and bounce to the at_exit block. This nonetheless doesn’t remedy our downside!

❯ ruby at_exit.rb                                                                                                     
[INFO 2022-04-04 21:00:40 -0400] #--- Beginning Employee Program ---#
[INFO 2022-04-04 21:00:40 -0400] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-04-04 21:00:45 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 21:00:46 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 21:00:47 -0400] Operation, iteration 0, full.
[INFO 2022-04-04 21:00:47 -0400] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
^C[WARN 2022-04-04 21:00:48 -0400] #--- Acquired interrupt ---#
at_exit.rb:5:in `sleep': Interrupt
from at_exit.rb:5:in `operation'
from at_exit.rb:35:in `<essential>'

Neither of those approaches works as a result of they will’t cease the principle thread from leaping out of execution to deal with the interrupt. As a substitute, we have to isolate the strategy execution from the principle thread in order that it’s merely paused by the interrupt.

To do that, we are able to use a second thread to run our ‘uninterruptable’ methodology, secure from interruption dealing with. We are able to then ‘clean-up’ our program in an at_exit block by ready for the completion of our uninterruptable methodology.

❯ ruby at_exit_uninterruptable.rb 
[INFO 2022-03-12 12:52:21 -0500] #--- Beginning Employee Program ---#
[INFO 2022-03-12 12:52:21 -0500] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-03-12 12:52:26 -0500] Operation half two. Eg. we would course of messages right here.
[INFO 2022-03-12 12:52:27 -0500] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-03-12 12:52:28 -0500] Operation, iteration 0, full.
[INFO 2022-03-12 12:52:28 -0500] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
^C[WARN 2022-03-12 12:52:30 -0500] #--- INTERRUPT RECEIVED ---#
[WARN 2022-03-12 12:52:30 -0500] Ready for present iteration to finish. Interrupt once more for fast (unsafe) termination.
[INFO 2022-03-12 12:52:33 -0500] Operation half two. Eg. we would course of messages right here.
[INFO 2022-03-12 12:52:34 -0500] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-03-12 12:52:35 -0500] Operation, iteration 1, full.
[INFO 2022-03-12 12:52:35 -0500] #--- ITERATION COMPLETED. EXITING SAFELY ---#
delay_script_exit.rb:51:in `be part of': Interrupt
from delay_script_exit.rb:51:in `<essential>'

It really works!

And in order for you prettier output on exit somewhat than a stacktrace, you can even halt the thread through a SignalException lure.

❯ ruby signal_trap_uninterruptable.rb
[INFO 2022-04-04 21:12:23 -0400] #--- Beginning Employee Program ---#
[INFO 2022-04-04 21:12:23 -0400] Begin operation, iteration 0. Half one: Eg. we would lengthy ballot for messages right here
[INFO 2022-04-04 21:12:28 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 21:12:29 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 21:12:30 -0400] Operation, iteration 0, full.
[INFO 2022-04-04 21:12:30 -0400] Begin operation, iteration 1. Half one: Eg. we would lengthy ballot for messages right here
^C[WARN 2022-04-04 21:12:33 -0400] #--- INTERRUPT RECEIVED ---#
[WARN 2022-04-04 21:12:33 -0400] Ready for present iteration to finish. Interrupt once more for fast (unsafe) termination.
[INFO 2022-04-04 21:12:35 -0400] Operation half two. Eg. we would course of messages right here.
[INFO 2022-04-04 21:12:36 -0400] Operation half three. Eg. we would acknowledge the message as processed right here.
[INFO 2022-04-04 21:12:37 -0400] Operation, iteration 1, full.
[INFO 2022-04-04 21:12:37 -0400] #--- ITERATION COMPLETED. EXITING SAFELY ---#

And that’s it! We’ve captured our sign interrupt and waited or our uninterruptable operation to finish earlier than exiting. Completely satisfied coding!

More Posts