Skip to content

Commit eefeaa0

Browse files
committed
First release of River Ruby bindings
A first push of Ruby bindings for River. Meant to be used in conjunction with a driver like `riverqueue-sequel` [1] to provide an insert-only client for River. See the README for details on usage. Overall, I'm happy at how close I was able to keep the API to the Go version. A lot of syntax in Go just isn't needed due to the more dynamic and implicit nature of Ruby, but the parts that came through are quite close. e.g. We have a job args concept, along with `InsertOpts` that can be added to both jobs and at insert time, just like Go. Purposely not implemented on this first push (I'll follow up with these later on): * Unique jobs. * Batch insert. [1] https://github.com/riverqueue/riverqueue-ruby-sequel
1 parent 7437932 commit eefeaa0

14 files changed

Lines changed: 760 additions & 16 deletions

File tree

.github/workflows/ci.yml

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
name: CI
2+
3+
env:
4+
# Database to connect to that can create other databases with `CREATE DATABASE`.
5+
ADMIN_DATABASE_URL: postgres://postgres:postgres@localhost:5432
6+
7+
# Just a common place for steps to put binaries they need and which is added
8+
# to GITHUB_PATH/PATH.
9+
BIN_PATH: /home/runner/bin
10+
11+
# A suitable URL for a test database.
12+
TEST_DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/riverqueue_ruby_test?sslmode=disable
13+
14+
on:
15+
- push
16+
17+
jobs:
18+
lint:
19+
runs-on: ubuntu-latest
20+
timeout-minutes: 3
21+
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@v4
25+
26+
- name: Install Ruby + `bundle install`
27+
uses: ruby/setup-ruby@v1
28+
with:
29+
ruby-version: "head"
30+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31+
32+
- name: Standard Ruby
33+
run: bundle exec standardrb
34+
35+
spec:
36+
runs-on: ubuntu-latest
37+
timeout-minutes: 3
38+
39+
services:
40+
postgres:
41+
image: postgres
42+
env:
43+
POSTGRES_PASSWORD: postgres
44+
options: >-
45+
--health-cmd pg_isready
46+
--health-interval 2s
47+
--health-timeout 5s
48+
--health-retries 5
49+
ports:
50+
- 5432:5432
51+
52+
steps:
53+
- name: Checkout
54+
uses: actions/checkout@v4
55+
56+
- name: Install Ruby + `bundle install`
57+
uses: ruby/setup-ruby@v1
58+
with:
59+
ruby-version: "head"
60+
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
61+
62+
# There is a version of Go on Actions' base image, but it's old and can't
63+
# read modern `go.mod` annotations correctly.
64+
- name: Install Go
65+
uses: actions/setup-go@v4
66+
with:
67+
go-version: "stable"
68+
check-latest: true
69+
70+
- name: Create database
71+
run: psql --echo-errors --quiet -c '\timing off' -c "CREATE DATABASE riverqueue_ruby_test;" ${ADMIN_DATABASE_URL}
72+
73+
- name: Install River CLI
74+
run: go install github.com/riverqueue/river/cmd/river@latest
75+
76+
- name: river migrate-up
77+
run: river migrate-up --database-url "$TEST_DATABASE_URL"
78+
79+
- name: Rspec
80+
run: bundle exec rspec

Gemfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
source "https://rubygems.org"
2+
3+
gemspec
4+
5+
group :development, :test do
6+
gem "standard"
7+
end
8+
9+
group :test do
10+
gem "rspec-core"
11+
gem "rspec-expectations"
12+
end

Gemfile.lock

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
PATH
2+
remote: .
3+
specs:
4+
riverqueue (0.0.1)
5+
6+
GEM
7+
remote: https://rubygems.org/
8+
specs:
9+
ast (2.4.2)
10+
diff-lcs (1.5.0)
11+
json (2.6.3)
12+
language_server-protocol (3.17.0.3)
13+
lint_roller (1.1.0)
14+
parallel (1.23.0)
15+
parser (3.2.2.4)
16+
ast (~> 2.4.1)
17+
racc
18+
racc (1.7.3)
19+
rainbow (3.1.1)
20+
regexp_parser (2.8.2)
21+
rexml (3.2.6)
22+
rspec-core (3.12.2)
23+
rspec-support (~> 3.12.0)
24+
rspec-expectations (3.12.3)
25+
diff-lcs (>= 1.2.0, < 2.0)
26+
rspec-support (~> 3.12.0)
27+
rspec-support (3.12.1)
28+
rubocop (1.57.2)
29+
json (~> 2.3)
30+
language_server-protocol (>= 3.17.0)
31+
parallel (~> 1.10)
32+
parser (>= 3.2.2.4)
33+
rainbow (>= 2.2.2, < 4.0)
34+
regexp_parser (>= 1.8, < 3.0)
35+
rexml (>= 3.2.5, < 4.0)
36+
rubocop-ast (>= 1.28.1, < 2.0)
37+
ruby-progressbar (~> 1.7)
38+
unicode-display_width (>= 2.4.0, < 3.0)
39+
rubocop-ast (1.30.0)
40+
parser (>= 3.2.1.0)
41+
rubocop-performance (1.19.1)
42+
rubocop (>= 1.7.0, < 2.0)
43+
rubocop-ast (>= 0.4.0)
44+
ruby-progressbar (1.13.0)
45+
standard (1.32.0)
46+
language_server-protocol (~> 3.17.0.2)
47+
lint_roller (~> 1.0)
48+
rubocop (~> 1.57.2)
49+
standard-custom (~> 1.0.0)
50+
standard-performance (~> 1.2)
51+
standard-custom (1.0.2)
52+
lint_roller (~> 1.0)
53+
rubocop (~> 1.50)
54+
standard-performance (1.2.1)
55+
lint_roller (~> 1.1)
56+
rubocop-performance (~> 1.19.1)
57+
unicode-display_width (2.5.0)
58+
59+
PLATFORMS
60+
arm64-darwin-22
61+
x86_64-linux
62+
63+
DEPENDENCIES
64+
riverqueue!
65+
rspec-core
66+
rspec-expectations
67+
standard
68+
69+
BUNDLED WITH
70+
2.4.20

README.md

Lines changed: 0 additions & 8 deletions
This file was deleted.

docs/README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# River client for Ruby [![Build Status](https://github.com/riverqueue/riverqueue-ruby/workflows/CI/badge.svg)](https://github.com/riverqueue/riverqueue-ruby/actions)
2+
3+
An insert-only Ruby client for [River](https://github.com/riverqueue/river) packaged in the [`riverqueue` gem](https://rubygems.org/gems/riverqueue). Allows jobs to be inserted in Ruby and run by a Go worker, but doesn't support working jobs in Ruby.
4+
5+
## Basic usage
6+
7+
`Gemfile` should contain the core gem and a driver like [`rubyqueue-sequel`](https://github.com/riverqueue/riverqueue-ruby-sequel):
8+
9+
``` yaml
10+
gem "riverqueue"
11+
gem "riverqueue-sequel"
12+
```
13+
14+
Initialize a client with:
15+
16+
```ruby
17+
DB = Sequel.connect("postgres://...")
18+
client = River::Client.new(River::Driver::Sequel.new(DB))
19+
```
20+
21+
Define a job and insert it:
22+
23+
```ruby
24+
class SortArgs
25+
attr_accessor :strings
26+
27+
def initialize(strings:)
28+
self.strings = strings
29+
end
30+
31+
def kind = "sort"
32+
33+
def to_json = JSON.dump({strings: strings})
34+
end
35+
36+
job = client.insert(SimpleArgs.new(strings: ["whale", "tiger", "bear"]))
37+
```
38+
39+
Job args should:
40+
41+
* Respond to `#kind` with a unique string that identifies them in the database, and which a Go worker will recognize.
42+
* Response to `#to_json` with a JSON serialization that'll be parseable in Go.
43+
44+
They may also respond to `#insert_opts` with an instance of `InsertOpts` to define insertion options that'll be used for all jobs of the kind.
45+
46+
### Insertion options
47+
48+
Inserts take an `insert_opts` parameter to customize features of the inserted job:
49+
50+
```ruby
51+
job = client.insert(
52+
SimpleArgs.new(strings: ["whale", "tiger", "bear"]),
53+
insert_opts: River::InsertOpts.new(
54+
max_attempts: 17,
55+
priority: 3,
56+
queue: "my_queue",
57+
tags: ["custom"]
58+
)
59+
)
60+
```
61+
62+
### Inserting with a Ruby hash
63+
64+
`JobArgsHash` can be used to insert with a kind and JSON hash so that it's not necessary to define a class:
65+
66+
```ruby
67+
job = client.insert(River::JobArgsHash.new("hash_kind", {
68+
job_num: 1
69+
}))
70+
```
71+
72+
## Development
73+
74+
See [development](./development.md).

docs/development.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# riverqueue-ruby development
2+
3+
## Install dependencies
4+
5+
```shell
6+
$ bundle install
7+
```
8+
## Run tests
9+
10+
Create a test database and migrate with River's CLI:
11+
12+
```shell
13+
$ go install github.com/riverqueue/river/cmd/river
14+
$ createdb riverqueue_ruby_test
15+
$ river migrate-up --database-url "postgres://localhost/riverqueue_ruby_test"
16+
```
17+
18+
Run all specs:
19+
20+
```shell
21+
$ bundle exec rspec spec
22+
```
23+
24+
## Run lint
25+
26+
```shell
27+
$ standardrb --fix
28+
```
29+
30+
## Publish a new gem
31+
32+
```shell
33+
git checkout master && git pull --rebase
34+
VERSION=v0.0.x
35+
gem build riverqueue.gemspec
36+
gem push riverqueue-$VERSION.gem
37+
git tag $VERSION
38+
git push --tags
39+
```

lib/client.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
module River
2+
MAX_ATTEMPTS_DEFAULT = 25
3+
PRIORITY_DEFAULT = 1
4+
QUEUE_DEFAULT = "default"
5+
6+
# Provides a client for River that inserts jobs. Unlike the Go version of the
7+
# River client, this one can insert jobs only. Jobs can only be worked from Go
8+
# code, so job arg kinds and JSON encoding details must be shared between Ruby
9+
# and Go code.
10+
#
11+
# Used in conjunction with a River driver like:
12+
#
13+
# DB = Sequel.connect(...)
14+
# client = River::Client.new(River::Driver::Sequel.new(DB))
15+
#
16+
# River drivers are found in separate gems like `riverqueue-sequel` to help
17+
# minimize transient dependencies.
18+
class Client
19+
def initialize(driver)
20+
@driver = driver
21+
end
22+
23+
# Inserts a new job for work given a job args implementation and insertion
24+
# options (which may be omitted).
25+
#
26+
# Job arg implementations are expected to respond to:
27+
#
28+
# * `#kind`: A string that uniquely identifies the job in the database.
29+
# * `#to_json`: Encodes the args to JSON for persistence in the database.
30+
# Must match encoding an args struct on the Go side to be workable.
31+
#
32+
# They may also respond to `#insert_opts` which is expected to return an
33+
# `InsertOpts` that contains options that will apply to all jobs of this
34+
# kind. Insertion options provided as an argument to `#insert` override
35+
# those returned by job args.
36+
def insert(args, insert_opts: InsertOpts.new)
37+
raise "args should respond to `#kind`" if !args.respond_to?(:kind)
38+
raise "args should respond to `#to_json`" if !args.respond_to?(:to_json)
39+
40+
args_insert_opts = args.respond_to?(:insert_opts) ? args.insert_opts : InsertOpts.new
41+
42+
scheduled_at = insert_opts.scheduled_at || args_insert_opts.scheduled_at
43+
44+
@driver.insert(Internal::JobInsertParams.new(
45+
encoded_args: args.to_json,
46+
kind: args.kind,
47+
max_attempts: insert_opts.max_attempts || args_insert_opts.max_attempts || MAX_ATTEMPTS_DEFAULT,
48+
priority: insert_opts.priority || args_insert_opts.priority || PRIORITY_DEFAULT,
49+
queue: insert_opts.queue || args_insert_opts.queue || QUEUE_DEFAULT,
50+
scheduled_at: scheduled_at, # database default to now
51+
state: scheduled_at ? JOB_STATE_SCHEDULED : JOB_STATE_AVAILABLE,
52+
tags: insert_opts.tags || args_insert_opts.tags
53+
))
54+
end
55+
end
56+
end

0 commit comments

Comments
 (0)