{"id":45591748,"url":"https://github.com/openjobspec/ojs-ruby-sdk","last_synced_at":"2026-02-28T17:07:52.202Z","repository":{"id":338688066,"uuid":"1156979317","full_name":"openjobspec/ojs-ruby-sdk","owner":"openjobspec","description":"Official Ruby SDK for the Open Job Spec (OJS) protocol.","archived":false,"fork":false,"pushed_at":"2026-02-23T09:45:56.000Z","size":167,"stargazers_count":0,"open_issues_count":5,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-23T19:28:22.757Z","etag":null,"topics":["background-jobs","job-queue","ojs","openjobspec","ruby","sdk","worker"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/openjobspec.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-13T09:30:33.000Z","updated_at":"2026-02-18T20:04:24.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/openjobspec/ojs-ruby-sdk","commit_stats":null,"previous_names":["openjobspec/ojs-ruby-sdk"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/openjobspec/ojs-ruby-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openjobspec%2Fojs-ruby-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openjobspec%2Fojs-ruby-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openjobspec%2Fojs-ruby-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openjobspec%2Fojs-ruby-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/openjobspec","download_url":"https://codeload.github.com/openjobspec/ojs-ruby-sdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/openjobspec%2Fojs-ruby-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29943807,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-28T13:49:17.081Z","status":"ssl_error","status_checked_at":"2026-02-28T13:48:50.396Z","response_time":90,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["background-jobs","job-queue","ojs","openjobspec","ruby","sdk","worker"],"created_at":"2026-02-23T12:42:13.161Z","updated_at":"2026-02-28T17:07:51.890Z","avatar_url":"https://github.com/openjobspec.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OJS Ruby SDK\n\n[![CI](https://github.com/openjobspec/ojs-ruby-sdk/actions/workflows/test.yml/badge.svg)](https://github.com/openjobspec/ojs-ruby-sdk/actions/workflows/test.yml)\n[![Gem Version](https://badge.fury.io/rb/ojs.svg)](https://rubygems.org/gems/ojs)\n[![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-ruby.svg)](https://www.ruby-lang.org/)\n[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)\n\nOfficial Ruby SDK for the [Open Job Spec (OJS)](https://openjobspec.org) protocol.\n\n**Zero runtime dependencies.** Uses only `net/http` and `json` from the Ruby standard library.\n\n\u003e 🎮 **New to OJS?** Try the [OJS Playground](https://github.com/openjobspec/ojs-playground) for an interactive exploration environment.\n\n\u003e **🚀 Try it now:** [Open in Playground](https://playground.openjobspec.org?lang=ruby) · [Run on CodeSandbox](https://codesandbox.io/p/sandbox/openjobspec-ruby-quickstart) · [Docker Quickstart](https://github.com/openjobspec/openjobspec/blob/main/docker-compose.quickstart.yml)\n\n## Requirements\n\n- Ruby 3.2+\n\n## Installation\n\nAdd to your Gemfile:\n\n```ruby\ngem \"ojs\"\n```\n\nOr install directly:\n\n```\ngem install ojs\n```\n\n## Quick Start\n\n### Client (Producer)\n\n```ruby\nrequire \"ojs\"\n\nclient = OJS::Client.new(\"http://localhost:8080\")\n\n# Simple enqueue — keyword args become the job payload\njob = client.enqueue(\"email.send\", to: \"user@example.com\")\nputs job.id  # =\u003e \"019461a8-...\"\n\n# Enqueue with options\njob = client.enqueue(\"report.generate\", { id: 42 },\n  queue: \"reports\",\n  delay: \"5m\",\n  retry: OJS::RetryPolicy.new(max_attempts: 5),\n  unique: OJS::UniquePolicy.new(keys: [\"type\", \"args\"], period: \"PT1H\")\n)\n\n# Batch enqueue\njobs = client.enqueue_batch([\n  { type: \"email.send\", args: { to: \"a@example.com\" } },\n  { type: \"email.send\", args: { to: \"b@example.com\" } },\n])\n```\n\n### Worker (Consumer)\n\n```ruby\nrequire \"ojs\"\n\nworker = OJS::Worker.new(\"http://localhost:8080\",\n  queues: %w[default email],\n  concurrency: 10\n)\n\nworker.register(\"email.send\") do |ctx|\n  to = ctx.job.args[\"to\"]\n  result = send_email(to)\n  { message_id: result.id }\nend\n\n# Middleware (Sidekiq/Rack-style)\nworker.use(\"logging\") do |ctx, \u0026nxt|\n  puts \"Processing #{ctx.job.type}\"\n  start = Time.now\n  result = nxt.call\n  puts \"Done in #{Time.now - start}s\"\n  result\nend\n\nworker.start  # Blocks until SIGTERM/SIGINT\n```\n\n### Workflows\n\n```ruby\n# Chain (sequential)\nclient.workflow(OJS.chain(\n  OJS::Step.new(type: \"data.fetch\", args: { url: \"https://...\" }),\n  OJS::Step.new(type: \"data.transform\", args: { format: \"csv\" }),\n  OJS::Step.new(type: \"data.upload\", args: { bucket: \"results\" }),\n  name: \"etl-pipeline\"\n))\n\n# Group (parallel)\nclient.workflow(OJS.group(\n  OJS::Step.new(type: \"export.csv\", args: { report_id: 1 }),\n  OJS::Step.new(type: \"export.pdf\", args: { report_id: 1 }),\n  name: \"multi-export\"\n))\n\n# Batch (parallel + callbacks)\nclient.workflow(OJS.batch(\n  [\n    OJS::Step.new(type: \"email.send\", args: { to: \"a@example.com\" }),\n    OJS::Step.new(type: \"email.send\", args: { to: \"b@example.com\" }),\n  ],\n  name: \"bulk-send\",\n  on_complete: OJS::Step.new(type: \"batch.report\", args: {}),\n  on_failure: OJS::Step.new(type: \"batch.alert\", args: {})\n))\n```\n\n## API Reference\n\n### OJS::Client\n\n| Method | Description |\n|--------|-------------|\n| `enqueue(type, args, **opts)` | Enqueue a single job |\n| `enqueue_batch(jobs)` | Enqueue multiple jobs atomically |\n| `workflow(definition)` | Create and start a workflow |\n| `get_job(id)` | Get a job by ID |\n| `cancel_job(id)` | Cancel a job |\n| `queues` | List all queues |\n| `queue_stats(name)` | Get queue statistics |\n| `pause_queue(name)` | Pause a queue |\n| `resume_queue(name)` | Resume a paused queue |\n| `dead_letter_jobs` | List dead letter jobs |\n| `retry_dead_letter(id)` | Retry a dead letter job |\n| `discard_dead_letter(id)` | Discard a dead letter job |\n| `health` | Server health check |\n\n### Enqueue Options\n\n| Option | Type | Description |\n|--------|------|-------------|\n| `queue:` | String | Target queue (default: `\"default\"`) |\n| `delay:` | String | Delay before execution (`\"5m\"`, `\"1h\"`, `\"PT30S\"`) |\n| `scheduled_at:` | String | ISO 8601 timestamp for scheduled execution |\n| `priority:` | Integer | Job priority (higher = higher priority) |\n| `timeout:` | Integer | Max execution time in seconds |\n| `retry:` | RetryPolicy | Retry configuration |\n| `unique:` | UniquePolicy | Deduplication configuration |\n| `meta:` | Hash | Arbitrary metadata |\n| `expires_at:` | String | ISO 8601 expiration timestamp |\n\n### OJS::RetryPolicy\n\n```ruby\nOJS::RetryPolicy.new(\n  max_attempts: 5,              # Total attempts (default: 3)\n  initial_interval: \"PT2S\",     # First retry delay (default: \"PT1S\")\n  backoff_coefficient: 2.0,     # Multiplier per attempt (default: 2.0)\n  max_interval: \"PT10M\",        # Max delay cap (default: \"PT5M\")\n  jitter: true,                 # Randomize delays (default: true)\n  non_retryable_errors: [\"validation.*\"],\n  on_exhaustion: \"dead_letter\"  # \"discard\" (default) or \"dead_letter\"\n)\n```\n\n### OJS::UniquePolicy\n\n```ruby\nOJS::UniquePolicy.new(\n  keys: [\"type\", \"queue\", \"args\"],  # Uniqueness dimensions\n  args_keys: [\"user_id\"],           # Filter args keys\n  period: \"PT1H\",                   # TTL window\n  on_conflict: \"reject\"             # \"reject\", \"replace\", \"ignore\"\n)\n```\n\n### OJS::Worker\n\n| Method | Description |\n|--------|-------------|\n| `register(type, \u0026handler)` | Register a handler for a job type |\n| `use(name, \u0026block)` | Add middleware |\n| `start` | Start processing (blocks) |\n| `stop` | Graceful shutdown |\n| `quiet` | Stop fetching, finish in-flight |\n\n### Worker Options\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `queues:` | `[\"default\"]` | Queues to consume from |\n| `concurrency:` | `5` | Number of worker threads |\n| `poll_interval:` | `2.0` | Seconds between polls |\n| `heartbeat_interval:` | `15.0` | Seconds between heartbeats |\n| `shutdown_timeout:` | `25.0` | Max seconds to wait on shutdown |\n| `logger:` | `Logger.new($stdout)` | Logger instance for worker output |\n\n### Middleware\n\nMiddleware follows the Sidekiq/Rack yield-based pattern:\n\n```ruby\nworker.use(\"timing\") do |ctx, \u0026nxt|\n  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)\n  result = nxt.call\n  elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start\n  puts \"#{ctx.job.type} completed in #{elapsed.round(3)}s\"\n  result\nend\n```\n\nThe middleware chain supports `add`, `prepend`, `insert_before`, `insert_after`, and `remove` for ordering control.\n\n### Error Handling\n\nAll errors inherit from `OJS::Error`:\n\n| Error Class | Code | Retryable? |\n|-------------|------|------------|\n| `OJS::ValidationError` | `invalid_request` | No |\n| `OJS::NotFoundError` | `not_found` | No |\n| `OJS::ConflictError` | `duplicate` | No |\n| `OJS::QueuePausedError` | `queue_paused` | Yes |\n| `OJS::RateLimitError` | `rate_limited` | Yes |\n| `OJS::ServerError` | `backend_error` | Yes |\n| `OJS::TimeoutError` | `timeout` | Yes |\n| `OJS::ConnectionError` | — | Yes |\n\n```ruby\nbegin\n  client.enqueue(\"email.send\", to: \"user@example.com\")\nrescue OJS::ConflictError =\u003e e\n  puts \"Duplicate job: #{e.existing_job_id}\"\nrescue OJS::RateLimitError =\u003e e\n  sleep(e.retry_after || 5)\n  retry\nrescue OJS::Error =\u003e e\n  puts \"#{e.code}: #{e.message} (retryable: #{e.retryable?})\"\nend\n```\n\n## Migrating from Sidekiq\n\nSee [examples/sidekiq_migration.rb](examples/sidekiq_migration.rb) for a complete migration guide.\n\n## Testing\n\nThe SDK includes a built-in testing module that lets you test job-enqueuing code without a running OJS server.\n\n### Setup\n\n```ruby\nrequire \"ojs\"\nrequire \"ojs/testing\"\n\n# Create a test client with a fake in-memory transport\ntransport = OJS::Testing.fake_transport\nclient = OJS::Client.new(\"http://unused\", transport: transport)\n```\n\n### Asserting Enqueued Jobs\n\n```ruby\n# Enqueue some jobs in your code under test\nclient.enqueue(\"email.send\", to: \"user@example.com\")\nclient.enqueue(\"report.generate\", { id: 42 }, queue: \"reports\")\n\n# Assert jobs were enqueued\nOJS::Testing.assert_enqueued(\"email.send\")\nOJS::Testing.assert_enqueued(\"email.send\", count: 1)\nOJS::Testing.assert_enqueued_on(\"reports\", \"report.generate\")\n\n# Inspect enqueued jobs directly\nstore = OJS::Testing.store\nstore.enqueued          # =\u003e [Job, Job, ...]\nstore.enqueued_types    # =\u003e [\"email.send\", \"report.generate\"]\nstore.jobs_for(\"email.send\")  # =\u003e [Job]\n```\n\n### Draining Jobs\n\n```ruby\n# Register handlers and drain enqueued jobs synchronously\nOJS::Testing.drain(\"email.send\") do |job|\n  EmailService.deliver(job.args.first)\nend\n```\n\n### Cleanup\n\n```ruby\n# In your test teardown\nOJS::Testing.store.clear\n```\n\n## Development\n\n```bash\nbundle install\nbundle exec rspec\n```\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md) for more details.\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenjobspec%2Fojs-ruby-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fopenjobspec%2Fojs-ruby-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fopenjobspec%2Fojs-ruby-sdk/lists"}