We are dependent on an external API with continuous connectivity.
That API occasionally goes down and is not as stable as we'd like (i.e. < 100%).
Notify customers BEFORE they contact us and manage expectations.
A worker service that monitors this API on a given interval, and provides status updates to admins and account representatives.
This class wraps our API monitoring processes with an endless loop
let's create that loop. 2 minutes between "pings" sounds good.
class Worker
SLEEP_DURATION = 2.minutes
def start
puts "Staring API status service..."
loop do
# Perform API actions here
sleep(SLEEP_DURATION)
end
end
end
Because ending gracefully is nice:
class Worker
SLEEP_DELAY = 2.minutes
SLEEP_DURATION = 1.second
def start
puts "Staring API status service..."
trap("TERM") do
Thread.new { puts "Exiting..." }
stop
end
trap("INT") do
Thread.new { puts "Exiting..." }
stop
end
loop do
# Perform API actions here
sleep(SLEEP_DURATION)
break if stop?
end
end
private
def stop
@exit = true
end
def stop?
@exit == true
end
end
Wait, 2 minutes before we can end gracefully?!? Let's pull it all together:
class Worker
SLEEP_DELAY = 2.minutes
SLEEP_DURATION = 1.second
def start
log "Staring API status service..."
bind_signal_traps
Kernel.loop do
# Perform API actions here
SLEEP_DELAY.times do
if stop?
break
end
Kernel.sleep(SLEEP_DURATION)
end
if stop?
break
end
end
end
private
def stop
@exit = true
end
def stop?
@exit == true
end
def bind_signal_traps
trap("TERM") do
Thread.new { log "Exiting..." }
stop
end
trap("INT") do
Thread.new { log "Exiting..." }
stop
end
end
def log(message)
Rails.logger.info(message)
end
end
Note:
Calling loop
and sleep
on Kernel
allows those methods to be testable.
Test yo classes!
require "rails_helper"
RSpec.describe Worker do
describe "#start" do
it "does what we want it to" do
stub_kernel_loop
Worker.new.start
# Expectations for what we're doing in the while loop
end
it "loops by the sleep delay" do
stub_kernel_loop
Worker.new.start
expect(Worker::SLEEP_DELAY).to have_received(:times)
end
it "sleeps between loop iterations" do
stub_kernel_loop
Worker.new.start
expect(Kernel).to have_received(:sleep).with(1)
end
def stub_kernel_loop
allow(Kernel).to receive(:loop) do |&block|
block.call
end
allow(Worker::SLEEP_DELAY).to receive(:times) do |&block|
block.call
end
allow(Kernel).to receive(:sleep)
end
end
end
A simple rake task that creates our class and starts the worker activity:
namespace :api do
task status: [:environment] do
Worker.new.start
end
end
Sure, we could just say rake api:status
but it needs to be run on a remote server as a daemon or standalone service. The executable:
Launch that rake task
#!/bin/sh
bundle exec rake api:status
Do we want to run this rake task in development? No. Because rate limits and resources and stuff.
#!/bin/sh
if [ "$RACK_ENV" != "development" ]; then
bundle exec rake api:status
else
echo "Skipping api service in development!!"
fi
Anyone use foreman start
? Yet another endless while loop... but with minimal CPU
#!/bin/sh
if [ "$RACK_ENV" != "development" ]; then
bundle exec rake api:status
else
echo "Skipping api service in development!!"
while(true)
do
sleep 60m # limits CPU usage
done
fi