Uncategorized

Ruby: Resque Jobs and Jitter with `resque-scheduler`

Resque is a background job processor for Ruby. Sometimes you need to do something that’ll take a long time and you don’t want that happening as part of the HTTP request lifecycle. It helps you do that.

But what happens when you want to do LOTS of things at once, you want to avoid the thundering herd problem by spreading that work out rather than doing it all at once.

Jitter is one way to do this, taking an example using Resque, say you want to recalculate something for products and you have 10,000 products. Here we can define a resque job to do the recalculation, taking in the product ID and then queue a job for each but we don’t want them to all start at the same time due to the load this would put on the DB. To fix that we add jitter, saying start these jobs somewhere between now and 120seconds time, picking a random duration per job.

To do this for Resque we can use resque-scheduler, which lets us queue work for the future, along with before_enqueue resque hook to let a job declare its jitter with, for example, @jitter_milliseconds = 10 on the job class.

Here is an example of this all wired up (note: We ended up using a slightly different code for the prod implementation due to requirements on our side, be sure to test the below works for you before adopting).

# typed: strict
class ApplicationJob
extend T::Sig
sig { params(args: T.untyped).returns(T::Boolean) }
def self.before_enqueue_application_job(*args)
# Unique key for this job
job_key = Digest::MD5.hexdigest(resque.encode(class: self.to_s, args:))
jitter_milliseconds = jitter_milliseconds()
# Is jitter enabled for this job?
if jitter_milliseconds > 0
# Has the jitter wait already been done?
jitter_completed = resque.redis.get("resque:jitter:#{job_key}")
# If not, enqueue the job again with a random jitter delay
if !jitter_completed
# Track that we've done the jitter
resque.redis.setex("resque:jitter:#{job_key}", jitter_milliseconds * 3, 1)
# Requires unsafe because of the `*args` splat which isn't supported by sorbet.
resque.enqueue_in(jitter_milliseconds, self, *args)
return false
end
end
sig { returns(Integer) }
def self.jitter_milliseconds
return 0 unless self.instance_variable_defined?(:@jitter_milliseconds)
max_jitter = self.instance_variable_get(:@jitter_milliseconds)
rand(max_jitter)
end
end
class TestJitterJob < ApplicationJob
@queue = :dummy
@jitter_milliseconds = 10
def self.perform(id)
# noop
end
end

Standard
Uncategorized

Atuin + Codespaces: Sync command history between Codespaces and local

In my normal day I work in my local machine, in codespaces and in devcontainers.

Recently I started using Atuin which is an awesome tool for syncing your command history. Go check it out, it’s really nice!

A really cool feature is you can search history in different categories, like commands on this host or commands in this directory. Here I set this up for Codespaces so the host is the name of the repository. This means my commands from any instance of that repo’s Codespace are sync and searchable when in another new or existing Codespace.

Atuin has a public sync server but I prefer to sync to a local instance I host. This means I need a bit of wiring up to make this work when in a codespace or devcontainer.

When using Codespaces I tend to do the following:

  1. Use Chezmoi to configure the Codespace automatically via my dotfiles
  2. Open VSCode connect to the Codespace
  3. Open Kitty and SSH to the Codespace with gh cli, to have a terminal window connected to the Codespace

With this flow I need a way to allow calls from within the Codespaces to connect to the Atuin server I’m running on my local network.

To do this I’m going to use SSH Reverse Port Forwarding, you’ve all probably forwarded a port from your local machine to a remote machine before but did you know it supports the other way around too? You can forward a port from the remote machine to a destination the local machine has access too.

The other bit to tackle is authentication from the Atuin instance in the Codespace to the local server. To get this wired up I’m using SCP to copy the session and key files up to the Codespace.

Then I can use the templating features in Chezmoi to customize my Atuin and zshrc files when running in a Codespace. The ATUIN_HOST_NAME env lets you override the host name the commands are tracked against. In my case I set this to codespace/$GITHUB_REPOSITORY so I can search commands by repo (neat feature of Atuin is you can view command history by host, cwd and more).

I want a nice way to launch this all, this should display a menu which lets me select a Codespace and have all of this done automatically from there. For me I’m using Rofi which is a nice launcher for linux but fzf or another would work fine here too.

As a bonus I also use LanguageTool spell/grammer checking service, again self hosting. This, via VSCode extension, gives me spell checking for my Markdown files. I can forward the ports for this in the same way as I did for Atuin.

Lastly, adding in a key binding for i3 and I can now WIN+P and select my codespace then get all the bits I want open with language tools and atuin hooked up. When I move to a new codespace I get to keep my command history 🎉

Putting this all together you get (files trimmed to only the relevant bits).

db_path = "~/.config/atuin/history.db"
dialect = "uk"
style = "compact"
inline_height = 25
invert = true
update_check = false
sync_frequency = "5m"
{{- if eq (env "CODESPACES") "true" }}
auto_sync = true
sync_address = "http://localhost:3123&quot;
session_path = "/workspaces/atuin_session"
key_path = "/workspaces/atuin_key"
{{- else }}
auto_sync = true
sync_address = "http://my-local-atuin-server&quot;
{{- end }}
show_preview = true
#!/bin/bash
set -e
function get_recent_workspaces()
{
gh codespace list –json name,gitStatus,lastUsedAt,repository \
–template "{{range .}}{{.repository}} {{.gitStatus.ref}} last used: {{ slice .lastUsedAt 8 }} _ {{.name}};{{end}}" \
| tr ';' '\n'
}
WORKSPACE=$( (echo empty; get_recent_workspaces) | rofi -dmenu -p "Select Codespace:")
if [ x"empty" = x"${WORKSPACE}" ]
then
code
elif [ -n "${WORKSPACE}" ]
then
NAME=$(echo $WORKSPACE | cut –delimiter="_" –fields=2)
echo "Opening codespace $NAME"
gh cs code –codespace $NAME –insiders
# Copy auth details for atuin
gh cs cp –codespace $NAME -e ~/.local/share/atuin/session 'remote:/workspaces/atuin_session'
gh cs cp –codespace $NAME -e ~/.local/share/atuin/key 'remote:/workspaces/atuin_key'
# atuin server languagetool grammer/spellcheck
kitty bash -c "gh codespace ssh –codespace $NAME — -R 3123:my-local-atuin-server:80 -R 8081:my-language-tools-server:8081"
fi
{{- if eq (env "CODESPACES") "true" }}
# Configures hostname and user for synced commands
ATUIN_HOST_NAME="codespace/$GITHUB_REPOSITORY"
ATUIN_HOST_USER=$GITHUB_USER
{{- else }}
Standard
Uncategorized

GivEnergy and Polybar: Show stats from Solar array in my status bar

This was a quick fun hack to pull back the data from my solar inverter and display it in my status bar.

Here’s the code

view raw Readme.md hosted with ❤ by GitHub
#!/usr/bin/env python3
import datetime
from givenergy_modbus.client import GivEnergyClient
from givenergy_modbus.model.plant import Plant
try:
client = GivEnergyClient(host="10.0.1.254")
p = Plant(number_batteries=1)
client.refresh_plant(p, full_refresh=True)
def format_color(color: str, text: str):
return "%{F" + color + "}" + text + "%{F-}"
def format_wattage(input: int):
if input > 1000:
return f"{input/1000}Kw"
return f"{input}w"
def format_icon(input: str):
return format_color("#F0C674", f" {input} ")
surplus = p.inverter.p_load_demand < (p.inverter.p_pv1 + p.inverter.p_pv2)
if surplus:
lightening_color = "#2d862d"
else:
lightening_color = "#b30000"
output = format_color(lightening_color, "")
output += format_icon("") + format_wattage(p.inverter.p_load_demand)
output += format_icon("󰶛") + format_wattage(p.inverter.p_pv1 + p.inverter.p_pv2)
output += format_icon("") + str(p.inverter.battery_percent) + "%"
output += format_icon("") + format_wattage(p.inverter.p_grid_out)
print(output)
except Exception as e:
print("Error talking to inverter")
view raw givenergy.py hosted with ❤ by GitHub
[module/givenergy]
type = custom/script
exec = $HOME/.config/polybar/givenergy.py
interval = 60
Standard