Skip to content

Commit c1617c3

Browse files
committed
Add fully functional Ruby client compatible with Go client
Here, try to do a full port of River to Ruby using a similar technique to Rust through repeated refinement iterations. I also had it try to go through each of River's features sections in the docs and make sure we have equivalent Ruby functionality. I changed a fair bit through manual inspection of the resulting APIs, but there's still a reasonable possibility we'll notice more things and want to make a few additional tweaks before fully committing. I also have a full Pro implementation locally, but have left it out of this change set. We'd ship this from a private gem server separately.
1 parent f29d1d4 commit c1617c3

188 files changed

Lines changed: 19539 additions & 696 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yaml‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,92 @@ env:
2323
on:
2424
- push
2525

26+
permissions:
27+
contents: read
28+
2629
jobs:
30+
conformance:
31+
runs-on: ubuntu-latest
32+
timeout-minutes: 15
33+
strategy:
34+
matrix:
35+
driver: [sequel, activerecord]
36+
env:
37+
RIVER_CONFORMANCE_DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/river_conformance?sslmode=disable
38+
RIVER_CONFORMANCE_DRIVER: ${{ matrix.driver }}
39+
services:
40+
postgres:
41+
image: postgres:17
42+
env:
43+
POSTGRES_DB: river_conformance
44+
POSTGRES_PASSWORD: postgres
45+
options: >-
46+
--health-cmd pg_isready
47+
--health-interval 2s
48+
--health-timeout 5s
49+
--health-retries 5
50+
ports:
51+
- 5432:5432
52+
steps:
53+
- uses: actions/checkout@v4
54+
- uses: ruby/setup-ruby@v1
55+
with:
56+
ruby-version: ${{ env.RUBY_VERSION }}
57+
bundler-cache: true
58+
- name: Read conformance reference revision
59+
id: reference
60+
run: |
61+
ruby -rjson -e '
62+
revision = JSON.parse(File.read("conformance/reference.json")).fetch("revision")
63+
abort "Invalid River revision" unless /\A[0-9a-f]{40}\z/.match?(revision)
64+
File.open(ENV.fetch("GITHUB_OUTPUT"), "a") { |file| file.puts("revision=#{revision}") }
65+
'
66+
- uses: actions/checkout@v4
67+
with:
68+
repository: riverqueue/river
69+
ref: ${{ steps.reference.outputs.revision }}
70+
path: upstream/river
71+
persist-credentials: false
72+
- uses: actions/setup-go@v5
73+
with:
74+
go-version-file: upstream/river/go.mod
75+
cache-dependency-path: upstream/river/**/go.sum
76+
- name: Required insert-only conformance
77+
run: make test/conformance/insert-only RIVER_PATH="$GITHUB_WORKSPACE/upstream/river"
78+
79+
verify:
80+
runs-on: ubuntu-latest
81+
timeout-minutes: 3
82+
83+
steps:
84+
- name: Checkout
85+
uses: actions/checkout@v4
86+
87+
- name: Install Ruby
88+
uses: ruby/setup-ruby@v1
89+
with:
90+
ruby-version: ${{ env.RUBY_VERSION }}
91+
92+
- name: Read upstream River revision
93+
id: river_revision
94+
run: |
95+
ruby -rjson -e '
96+
revision = JSON.parse(File.read("migration/manifest.json")).fetch("revision")
97+
abort "Invalid River revision" unless /\A[0-9a-f]{40}\z/.match?(revision)
98+
File.open(ENV.fetch("GITHUB_OUTPUT"), "a") { |file| file.puts("revision=#{revision}") }
99+
'
100+
101+
- name: Checkout upstream River
102+
uses: actions/checkout@v4
103+
with:
104+
path: upstream/river
105+
persist-credentials: false
106+
ref: ${{ steps.river_revision.outputs.revision }}
107+
repository: riverqueue/river
108+
109+
- name: Verify migrations
110+
run: make verify RIVER_PATH="$GITHUB_WORKSPACE/upstream/river"
111+
27112
gem_build:
28113
runs-on: ubuntu-latest
29114
timeout-minutes: 3
@@ -50,6 +135,10 @@ jobs:
50135
run: gem build riverqueue-sequel.gemspec
51136
working-directory: ./driver/riverqueue-sequel
52137

138+
- name: Build gem (riverqueue-rails)
139+
run: gem build riverqueue-rails.gemspec
140+
working-directory: ./rails/riverqueue-rails
141+
53142
lint:
54143
runs-on: ubuntu-latest
55144
timeout-minutes: 3
@@ -68,6 +157,10 @@ jobs:
68157
run: bundle exec standardrb
69158
working-directory: .
70159

160+
- name: Frozen string literal comments
161+
run: bundle exec rubocop --config .rubocop-frozen-string-literal.yaml --only Style/FrozenStringLiteralComment
162+
working-directory: .
163+
71164
- name: bundle install (riverqueue-activerecord)
72165
run: bundle install
73166
working-directory: ./driver/riverqueue-activerecord
@@ -195,3 +288,44 @@ jobs:
195288
- name: Rspec (riverqueue-sequel)
196289
run: bundle exec rspec
197290
working-directory: ./driver/riverqueue-sequel
291+
292+
rails:
293+
runs-on: ubuntu-latest
294+
timeout-minutes: 5
295+
strategy:
296+
matrix:
297+
include:
298+
- rails: "~> 7.2.0"
299+
ruby: "3.2"
300+
- rails: "~> 8.0.0"
301+
ruby: "3.3"
302+
- rails: "~> 8.1.0"
303+
ruby: "4.0"
304+
env:
305+
BUNDLE_FROZEN: "false"
306+
RAILS_VERSION: ${{ matrix.rails }}
307+
RIVER_REQUIRE_DATABASES: "1"
308+
services:
309+
postgres:
310+
image: postgres:17
311+
env:
312+
POSTGRES_DB: river_test
313+
POSTGRES_PASSWORD: postgres
314+
options: >-
315+
--health-cmd pg_isready
316+
--health-interval 2s
317+
--health-timeout 5s
318+
--health-retries 5
319+
ports:
320+
- 5432:5432
321+
steps:
322+
- uses: actions/checkout@v4
323+
- uses: ruby/setup-ruby@v1
324+
with:
325+
ruby-version: ${{ matrix.ruby }}
326+
- run: bundle install
327+
working-directory: ./rails/riverqueue-rails
328+
- run: bundle exec rspec
329+
working-directory: ./rails/riverqueue-rails
330+
- run: bundle exec standardrb
331+
working-directory: ./rails/riverqueue-rails
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
AllCops:
2+
NewCops: disable
3+
Exclude:
4+
- ".ruby-lsp/**/*"
5+
- "**/coverage/**/*"
6+
- "**/vendor/**/*"
7+
8+
Style/FrozenStringLiteralComment:
9+
Enabled: true
10+
EnforcedStyle: always

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add a full Ruby client for River with Go-compatible job insertion and execution on PostgreSQL, SQLite, and YugabyteDB through ActiveRecord or Sequel. Includes workers, retries, cancellation, periodic and resumable jobs, job-persisted logging, job administration, migration and worker CLIs, and testing helpers. Rails and Active Job integration is available through `riverqueue-rails`, with workflows, batches, sequences, concurrency controls, and other advanced features in the separately distributed `riverqueue-pro` gem. [PR #70](https://github.com/riverqueue/riverqueue-ruby/pull/70).
13+
1014
## [0.11.0] - 2026-09-02
1115

1216
### Added

‎Gemfile‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# frozen_string_literal: true
2+
13
source "https://rubygems.org"
24

35
gemspec
@@ -9,9 +11,12 @@ end
911

1012
group :test do
1113
gem "debug"
14+
gem "fugit", "~> 1.13", require: false
15+
gem "minitest", require: false
1216
gem "pg"
1317
gem "rspec-core"
1418
gem "rspec-expectations"
19+
gem "riverqueue-activerecord", path: "driver/riverqueue-activerecord"
1520
gem "riverqueue-sequel", path: "driver/riverqueue-sequel"
1621
gem "simplecov", require: false
1722
gem "sqlite3"

‎Gemfile.lock‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,35 @@ PATH
22
remote: .
33
specs:
44
riverqueue (0.11.0)
5+
logger (> 0, < 1000)
6+
optparse (> 0, < 1000)
7+
securerandom (> 0, < 1000)
8+
timeout (> 0, < 1000)
9+
10+
PATH
11+
remote: driver/riverqueue-activerecord
12+
specs:
13+
riverqueue-activerecord (0.11.0)
14+
activerecord (> 0, < 1000)
15+
activesupport (> 0, < 1000)
16+
riverqueue (= 0.11.0)
517

618
PATH
719
remote: driver/riverqueue-sequel
820
specs:
921
riverqueue-sequel (0.11.0)
22+
riverqueue (= 0.11.0)
1023
sequel (> 0, < 1000)
1124

1225
GEM
1326
remote: https://rubygems.org/
1427
specs:
28+
activemodel (8.1.3)
29+
activesupport (= 8.1.3)
30+
activerecord (8.1.3)
31+
activemodel (= 8.1.3)
32+
activesupport (= 8.1.3)
33+
timeout (>= 0.4.0)
1534
activesupport (8.1.3)
1635
base64
1736
bigdecimal
@@ -39,9 +58,14 @@ GEM
3958
docile (1.4.1)
4059
drb (2.2.3)
4160
erb (6.0.4)
61+
et-orbi (1.4.2)
62+
tzinfo
4263
ffi (1.17.4-arm64-darwin)
4364
ffi (1.17.4-x86_64-linux-gnu)
4465
fileutils (1.8.0)
66+
fugit (1.13.0)
67+
et-orbi (~> 1.4)
68+
raabro (~> 1.4)
4569
i18n (1.14.8)
4670
concurrent-ruby (~> 1.0)
4771
io-console (0.8.2)
@@ -62,6 +86,7 @@ GEM
6286
drb (~> 2.0)
6387
prism (~> 1.5)
6488
mutex_m (0.3.0)
89+
optparse (0.8.1)
6590
parallel (1.27.0)
6691
parser (3.3.11.1)
6792
ast (~> 2.4.1)
@@ -75,6 +100,7 @@ GEM
75100
psych (5.3.1)
76101
date
77102
stringio
103+
raabro (1.5.0)
78104
racc (1.8.1)
79105
rainbow (3.1.1)
80106
rb-fsevent (0.11.2)
@@ -159,6 +185,7 @@ GEM
159185
strscan (3.1.7)
160186
terminal-table (4.0.0)
161187
unicode-display_width (>= 1.1.1, < 4)
188+
timeout (0.6.1)
162189
tsort (0.2.0)
163190
tzinfo (2.0.6)
164191
concurrent-ruby (~> 1.0)
@@ -173,8 +200,11 @@ PLATFORMS
173200

174201
DEPENDENCIES
175202
debug
203+
fugit (~> 1.13)
204+
minitest
176205
pg
177206
riverqueue!
207+
riverqueue-activerecord!
178208
riverqueue-sequel!
179209
rspec-core
180210
rspec-expectations
@@ -184,4 +214,4 @@ DEPENDENCIES
184214
steep
185215

186216
BUNDLED WITH
187-
2.6.7
217+
4.0.9

‎Makefile‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
.DEFAULT_GOAL := help
22

3+
RIVER_PATH ?= ../river
4+
RIVERQUEUE_PRO_PATH ?= ../riverqueue-ruby-pro
5+
6+
.PHONY: test/conformance/insert-only
7+
test/conformance/insert-only: ## Run the pinned Go insert-only contract against Ruby
8+
RIVER_PATH="$(RIVER_PATH)" bundle exec ruby conformance/run.rb
9+
10+
.PHONY: test/yugabyte
11+
test/yugabyte: ## Run both SQL adapters against a real Yugabyte database
12+
@test -n "$(YUGABYTE_DATABASE_URL)" || { echo "Set YUGABYTE_DATABASE_URL to a disposable test database"; exit 1; }
13+
cd driver/riverqueue-activerecord && TEST_DATABASE_URL="$(YUGABYTE_DATABASE_URL)" RIVER_YUGABYTE_TEST=1 RIVER_REQUIRE_DATABASES=1 RIVERQUEUE_ROOT_TEST_SUITE=1 bundle exec rspec spec/yugabyte_spec.rb
14+
cd driver/riverqueue-sequel && TEST_DATABASE_URL="$(YUGABYTE_DATABASE_URL)" RIVER_YUGABYTE_TEST=1 RIVER_REQUIRE_DATABASES=1 RIVERQUEUE_ROOT_TEST_SUITE=1 bundle exec rspec spec/yugabyte_spec.rb
15+
316
# Looks at comments using ## on targets and uses them to produce a help output.
417
.PHONY: help
518
help: ALIGN=14
@@ -11,9 +24,11 @@ install: ## Run `bundle install` on gem and all subgems
1124
bundle install
1225
cd driver/riverqueue-activerecord && bundle install
1326
cd driver/riverqueue-sequel && bundle install
27+
cd rails/riverqueue-rails && bundle install
28+
@if [ -f "$(RIVERQUEUE_PRO_PATH)/riverqueue-pro.gemspec" ]; then $(MAKE) -C "$(RIVERQUEUE_PRO_PATH)" install; fi
1429

1530
.PHONY: lint
16-
lint: standardrb ## Run linter (standardrb) on gem and all subgems
31+
lint: standardrb frozen-string-literals ## Run linters on gem and all subgems
1732

1833
.PHONY: rspec
1934
rspec: spec
@@ -23,12 +38,21 @@ spec:
2338
bundle exec rspec
2439
cd driver/riverqueue-activerecord && bundle exec rspec
2540
cd driver/riverqueue-sequel && bundle exec rspec
41+
cd rails/riverqueue-rails && bundle exec rspec
42+
@if [ -d driver/riverqueue-redis ]; then cd driver/riverqueue-redis && bundle exec rspec; fi
43+
@if [ -f "$(RIVERQUEUE_PRO_PATH)/riverqueue-pro.gemspec" ]; then $(MAKE) -C "$(RIVERQUEUE_PRO_PATH)" test; fi
2644

2745
.PHONY: standardrb
2846
standardrb:
2947
bundle exec standardrb --fix
3048
cd driver/riverqueue-activerecord && bundle exec standardrb --fix
3149
cd driver/riverqueue-sequel && bundle exec standardrb --fix
50+
cd rails/riverqueue-rails && bundle exec standardrb --fix
51+
@if [ -f "$(RIVERQUEUE_PRO_PATH)/riverqueue-pro.gemspec" ]; then $(MAKE) -C "$(RIVERQUEUE_PRO_PATH)" standardrb; fi
52+
53+
.PHONY: frozen-string-literals
54+
frozen-string-literals:
55+
bundle exec rubocop --config .rubocop-frozen-string-literal.yaml --only Style/FrozenStringLiteralComment
3256

3357
.PHONY: steep
3458
steep:
@@ -45,3 +69,9 @@ update: ## Run `bundle update` on gem and all subgems
4569
bundle update
4670
cd driver/riverqueue-activerecord && bundle update
4771
cd driver/riverqueue-sequel && bundle update
72+
cd rails/riverqueue-rails && bundle update
73+
@if [ -f "$(RIVERQUEUE_PRO_PATH)/riverqueue-pro.gemspec" ]; then $(MAKE) -C "$(RIVERQUEUE_PRO_PATH)" update; fi
74+
75+
.PHONY: verify
76+
verify: ## Verify bundled migrations against RIVER_PATH (default ../river)
77+
ruby scripts/sync_migrations.rb --check "$(RIVER_PATH)"

‎Steepfile‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
1+
# frozen_string_literal: true
2+
13
D = Steep::Diagnostic
24

35
target :lib do
46
check "lib"
57

68
library "digest"
79
library "json"
10+
library "logger"
11+
library "optparse"
12+
library "securerandom"
13+
library "socket"
814
library "time"
15+
library "timeout"
916

1017
signature "sig"
1118

0 commit comments

Comments
 (0)