wurk 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +15 -13
- data/app/controllers/wurk/api_controller.rb +49 -10
- data/config/routes.rb +20 -12
- data/lib/active_job/queue_adapters/wurk_adapter.rb +8 -2
- data/lib/sidekiq/rails.rb +5 -0
- data/lib/wurk/api/read_only.rb +1 -1
- data/lib/wurk/batch/server_middleware.rb +27 -0
- data/lib/wurk/capsule.rb +57 -2
- data/lib/wurk/cli.rb +4 -2
- data/lib/wurk/client.rb +16 -11
- data/lib/wurk/compat.rb +12 -2
- data/lib/wurk/configuration.rb +86 -7
- data/lib/wurk/encryption.rb +47 -8
- data/lib/wurk/fetcher/private_list_key.rb +19 -2
- data/lib/wurk/fetcher/reaper.rb +36 -12
- data/lib/wurk/fetcher/unparseable_keys.rb +22 -7
- data/lib/wurk/fetcher.rb +6 -1
- data/lib/wurk/health.rb +5 -1
- data/lib/wurk/job_logger.rb +1 -1
- data/lib/wurk/job_util.rb +4 -1
- data/lib/wurk/launcher.rb +4 -3
- data/lib/wurk/lua.rb +1 -14
- data/lib/wurk/metrics/dashboard_series.rb +21 -2
- data/lib/wurk/metrics/history.rb +13 -5
- data/lib/wurk/metrics/query.rb +9 -2
- data/lib/wurk/metrics/statsd.rb +8 -0
- data/lib/wurk/middleware/chain.rb +85 -13
- data/lib/wurk/pool_checkout.rb +1 -1
- data/lib/wurk/queue_slot.rb +19 -9
- data/lib/wurk/rails_boot.rb +4 -3
- data/lib/wurk/railtie.rb +7 -3
- data/lib/wurk/redis_connection.rb +4 -2
- data/lib/wurk/unique.rb +11 -8
- data/lib/wurk/version.rb +1 -1
- data/lib/wurk/web/config.rb +42 -9
- data/lib/wurk/web/enterprise.rb +4 -3
- data/lib/wurk/web/extension.rb +74 -39
- data/lib/wurk.rb +5 -2
- data/vendor/assets/dashboard/assets/{ArgsValue-_MYkd_vA.js → ArgsValue-DDqX6XWH.js} +1 -1
- data/vendor/assets/dashboard/assets/{BatchDetail-DPmg-hle.js → BatchDetail-lIytcn2C.js} +1 -1
- data/vendor/assets/dashboard/assets/{Batches-NTgqSlvD.js → Batches-BEumt-1O.js} +1 -1
- data/vendor/assets/dashboard/assets/{Busy-2rxcLJMq.js → Busy-Vk9HmZcN.js} +1 -1
- data/vendor/assets/dashboard/assets/{Cron-BFpJgKYb.js → Cron-CWcM-Cfr.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dashboard-C8VOE2S7.js → Dashboard-Bgv1kusv.js} +1 -1
- data/vendor/assets/dashboard/assets/{Dead-Dse2YEbZ.js → Dead-Di5Rct2H.js} +1 -1
- data/vendor/assets/dashboard/assets/Extension-LZY5-imc.js +1 -0
- data/vendor/assets/dashboard/assets/FilterBox-BDo-7IAy.js +2 -0
- data/vendor/assets/dashboard/assets/{FlowDetail-BWddOtgf.js → FlowDetail-CehwRLTt.js} +1 -1
- data/vendor/assets/dashboard/assets/{FlowState-6lGsojxE.js → FlowState-glK2Tnf8.js} +1 -1
- data/vendor/assets/dashboard/assets/{Flows-BgKTlYwI.js → Flows-cgy-Fsbp.js} +1 -1
- data/vendor/assets/dashboard/assets/{JobDetailModal-BrjmO98c.js → JobDetailModal-Cuk3jruU.js} +1 -1
- data/vendor/assets/dashboard/assets/{Limiters-DeUkfbcg.js → Limiters-Ci8G2yZG.js} +1 -1
- data/vendor/assets/dashboard/assets/Metrics-CQFUeZTR.js +1 -0
- data/vendor/assets/dashboard/assets/{NotFound-CQLLXIfa.js → NotFound-CYjhuDsI.js} +1 -1
- data/vendor/assets/dashboard/assets/{PageHeader-CeSaqysY.js → PageHeader-DjIAFTyX.js} +1 -1
- data/vendor/assets/dashboard/assets/{Profiles-CI2i2tl8.js → Profiles-DTowqIRg.js} +1 -1
- data/vendor/assets/dashboard/assets/{Queues-DUCQ_3St.js → Queues-DUGRPjME.js} +1 -1
- data/vendor/assets/dashboard/assets/{Retries-Dc2RFuTf.js → Retries-SJ3BaKC0.js} +1 -1
- data/vendor/assets/dashboard/assets/{Scheduled-B8zOKCLC.js → Scheduled-DxSRpmFZ.js} +1 -1
- data/vendor/assets/dashboard/assets/{Search-x0eS-V-U.js → Search-bBbi5JNK.js} +1 -1
- data/vendor/assets/dashboard/assets/{charts-VggSMFGc.js → charts-Bg2YBcaD.js} +1 -1
- data/vendor/assets/dashboard/assets/i18n-BZClP62Q.js +1 -0
- data/vendor/assets/dashboard/assets/index-BOTVSP9M.js +141 -0
- data/vendor/assets/dashboard/assets/index-Rewk4eSE.css +1 -0
- data/vendor/assets/dashboard/assets/{useResetPageOnEmpty-BsYWeStI.js → useResetPageOnEmpty-BRPNyV2M.js} +1 -1
- data/vendor/assets/dashboard/assets/{useSort-BWWfpnZH.js → useSort-B-BLJik7.js} +1 -1
- data/vendor/assets/dashboard/assets/{utils-DnzJXljj.js → utils-CGyrKpTh.js} +1 -1
- data/vendor/assets/dashboard/index.html +3 -3
- data/vendor/assets/dashboard/wurk-manifest.json +2 -2
- metadata +30 -44
- data/vendor/assets/dashboard/assets/Extension-BIc2c1zG.js +0 -1
- data/vendor/assets/dashboard/assets/FilterBox-BfZm8ZzH.js +0 -1
- data/vendor/assets/dashboard/assets/Metrics-DzmAgBFG.js +0 -1
- data/vendor/assets/dashboard/assets/i18n-B6AbnmJn.js +0 -1
- data/vendor/assets/dashboard/assets/index-CTWV8VL4.js +0 -141
- data/vendor/assets/dashboard/assets/index-WpZjU076.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5540591682172d88b9972b1cfc33cfca88c9c88d823c0b50fe80cb3556e8c08
|
|
4
|
+
data.tar.gz: a19e9d8e68fd2f9f93131cfad4f845595bc95f45aacb65047da452da52916cdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0d264b8039051e56d2f6cec5aaebb4ce1f99d62ad498a247bece1b6464c316685f43562a76c87ec46f31e846ff6cb6d536f51a11b6d9d06348c32c999d4f2be5
|
|
7
|
+
data.tar.gz: 9f695bfc7da4e5ad9e24e6874431a115b80bf3dab52f175a11b8f383d0586694b2d611f3137b6698eb896c1c2107ef18e97d33b7877a1a4fd3af60f380f0f47a
|
data/README.md
CHANGED
|
@@ -24,13 +24,13 @@ Wurk is wire-compatible with Sidekiq — same Redis keys, same job JSON, same Ru
|
|
|
24
24
|
|
|
25
25
|
**In production:** Wurk runs the background work at [developerz.ai](https://developerz.ai) and at partner deployments, across many servers, on the fork-based swarm described below. It is not a preview. Throughput numbers from those deployments are not published yet; what has been measured is in [docs/benchmarks.md](docs/benchmarks.md).
|
|
26
26
|
|
|
27
|
-
**At scale:** Wurk is built for fleets, not just for one box. Kubernetes `/live` + `/ready` probes are a config line, not a sidecar; a bearer-scoped [HTTP API](docs/api-http.md) lets non-Ruby services enqueue and inspect; [OpenTelemetry](docs/telemetry.md) traces propagate client → server; per-queue [global concurrency caps](docs/rate-limiting.md) hold cluster-wide limits; and monitoring is the dashboard you already mount — live SSE, charts,
|
|
27
|
+
**At scale:** Wurk is built for fleets, not just for one box. Kubernetes `/live` + `/ready` probes are a config line, not a sidecar; a bearer-scoped [HTTP API](docs/api-http.md) lets non-Ruby services enqueue and inspect; [OpenTelemetry](docs/telemetry.md) traces propagate client → server; per-queue [global concurrency caps](docs/rate-limiting.md) hold cluster-wide limits; and monitoring is the dashboard you already mount — live SSE, charts, no separate stack to run. See [Wurk extras](#wurk-extras).
|
|
28
28
|
|
|
29
|
-
**AI-managed:** Wurk is a 100% AI-managed project.
|
|
29
|
+
**AI-managed:** Wurk is a 100% AI-managed project. Every author's issues and pull requests, org members' included, and dependency updates, are triaged, reviewed, merged or closed by the [developerz.ai](https://developerz.ai) maintainer agent, which asks a human only for security reports and license questions, and cuts a release when a shipped fix has waited for one. New features are still built by us, humans and AI together — see [CONTRIBUTING.md](CONTRIBUTING.md) and [AI-maintained](#ai-maintained). The agent always identifies itself as a bot: it works publicly as `developerz-ai[bot]` and marks every comment it posts as bot-made. Its work and its rules are public too — browse the [repository](https://github.com/developerz-ai/wurk) and read the committed policy in [.dz/maintainer/maintainer.yml](.dz/maintainer/maintainer.yml).
|
|
30
30
|
|
|
31
31
|
**On Sidekiq:** Wurk implements Sidekiq's API because it is a genuinely good API. Sidekiq is human-maintained and funds that work through its paid tiers; Wurk is AI-maintained, which is what lets the same surface be free software. Wurk is independent and not affiliated with or endorsed by Sidekiq or its maintainers — see [Why Wurk exists](#why-wurk-exists).
|
|
32
32
|
|
|
33
|
-
**On speed:**
|
|
33
|
+
**On speed:** The latest published paired measurement (2026-08-07) put Wurk at roughly 0.87×–1.02× of stock Sidekiq depending on workload shape: parity on CPU and I/O, behind on framework overhead (noop) and boot time. This is dated evidence, not a throughput guarantee for today's gem. Numbers, method, and the reproduction command are in [docs/benchmarks.md](docs/benchmarks.md); run them yourself with `rake bench:vs_sidekiq`.
|
|
34
34
|
|
|
35
35
|
**Also from us:** [Ultimate](https://github.com/developerz-ai/ultimate) — the AI-first full-stack framework, for a weekend pet project or the system a company runs on. What a whole stack looks like when the one writing the code is an agent, not a human: [`bunx create-ultimate myapp`](#also-from-developerzai-ultimate).
|
|
36
36
|
|
|
@@ -98,7 +98,7 @@ Sidekiq has no equivalent for any of these — they aren't parity, they're new s
|
|
|
98
98
|
|
|
99
99
|
| Extra | What it does | Give up if you migrate back to Sidekiq |
|
|
100
100
|
|---|---|---|
|
|
101
|
-
| **[Job status, progress & results](docs/job-status.md)** | Opt-in `sidekiq_options track: true` persists a `status:<jid>` row — state, coalesced progress writes, the return value (size-capped, withheld under encryption) | `Wurk::Status` reads/writes and the
|
|
101
|
+
| **[Job status, progress & results](docs/job-status.md)** | Opt-in `sidekiq_options track: true` persists a `status:<jid>` row — state, coalesced progress writes, the return value (size-capped, withheld under encryption) | `Wurk::Status` reads/writes and the `/v1` API's job-status reader (`GET /jobs/:jid`) |
|
|
102
102
|
| **[HTTP producer + observe API](docs/api-http.md)** | A bearer-token-scoped `/v1` JSON API — enqueue, bulk-enqueue, inspect queues/jobs/swarm — mountable standalone, nested in the engine, or via the `wurk api` CLI | The whole `/v1` surface; non-Ruby producers lose their enqueue/inspect path |
|
|
103
103
|
| **[OpenTelemetry tracing](docs/telemetry.md)** | W3C `traceparent`/`tracestate` propagated client → server, one span per attempt, linked (not force-parented) across long delays | Distributed traces across your job graph |
|
|
104
104
|
| **[Flows — DAG-on-batches](docs/flows.md)** | `Wurk::Flow` chains and fans batches out/in with dependency edges, piped results between nodes, cycle/depth/width limits | The DAG builder, `pipe:` result-passing, `Flow.abandon` |
|
|
@@ -113,7 +113,7 @@ AI dashboard panes — anomaly detection, natural-language queries, error triage
|
|
|
113
113
|
|
|
114
114
|
## Benchmarks
|
|
115
115
|
|
|
116
|
-
**
|
|
116
|
+
**The latest published comparison does not show Wurk ahead of stock Sidekiq.** Measured 2026-08-07 — wurk 1.4.0 vs sidekiq 8.1.6, ruby 3.4.7, local Redis 7.4.10, 5000 jobs/run, 12 runs per topology, paired per-run ratios. These are historical results, not a fresh measurement of today's gem.
|
|
117
117
|
|
|
118
118
|
<picture>
|
|
119
119
|
<source media="(prefers-color-scheme: dark)" srcset="docs/assets/bench-throughput-dark.svg">
|
|
@@ -134,8 +134,8 @@ Method, per-invocation records, workload definitions, and the separate `rake ben
|
|
|
134
134
|
## Documentation
|
|
135
135
|
|
|
136
136
|
- **[Website](https://developerz-ai.github.io/wurk/)** · **[Wiki](https://github.com/developerz-ai/wurk/wiki)** — the pitch, install, and short orientation guides; the complete reference is [`docs/`](https://github.com/developerz-ai/wurk/tree/main/docs), linked from every wiki page.
|
|
137
|
-
- **[API reference (YARD)](https://developerz-ai.github.io/wurk/api/)** — generated
|
|
138
|
-
- **[Getting started & architecture](https://github.com/developerz-ai/wurk/blob/main/docs/idea/
|
|
137
|
+
- **[API reference (YARD)](https://developerz-ai.github.io/wurk/api/)** — generated from all of `lib/` (per `.yardopts`), so it documents internal classes (`Wurk::Manager`, `Wurk::Fetcher`, `Wurk::Processor`, `Wurk::Swarm`, the `Wurk::API::*` modules, …) as well as public ones; appearing in it does not make something supported public API. The supported public surface is defined by the parity specs ([`docs/target/sidekiq-free.md`](https://github.com/developerz-ai/wurk/blob/main/docs/target/sidekiq-free.md) · [Pro](https://github.com/developerz-ai/wurk/blob/main/docs/target/sidekiq-pro.md) · [Enterprise](https://github.com/developerz-ai/wurk/blob/main/docs/target/sidekiq-ent.md)). Machine-readable map for AI agents: **[llms.txt](https://developerz-ai.github.io/wurk/llms.txt)**.
|
|
138
|
+
- **[Getting started & architecture](https://github.com/developerz-ai/wurk/blob/main/docs/idea/02-architecture.md)** — how the swarm, manager, fetcher, and processor fit together.
|
|
139
139
|
- **[Starting the worker](https://github.com/developerz-ai/wurk/blob/main/docs/running.md)** — Rails auto-start, the `wurk`/`wurkswarm` runners, and running standalone without Rails.
|
|
140
140
|
- **[Configuration reference](https://github.com/developerz-ai/wurk/blob/main/docs/configuration.md)** — every option, env var, YAML key, and CLI flag, with precedence and pool sizing.
|
|
141
141
|
- **[Deploying](https://github.com/developerz-ai/wurk/blob/main/docs/deployment.md)** — systemd, Capistrano, Heroku, Docker, Kubernetes, rolling restarts, memory limits.
|
|
@@ -165,6 +165,8 @@ Method, per-invocation records, workload definitions, and the separate `rake ben
|
|
|
165
165
|
| Ruby | `>= 3.2.0` |
|
|
166
166
|
| Redis | `>= 7.0.0` |
|
|
167
167
|
|
|
168
|
+
The full test suite also runs as a non-required `compat` workflow job across Ruby 3.2.x and the newest Ruby on the current Rails series, the oldest supported Rails series on the newest Ruby, and Valkey 8 as a drop-in store alongside Redis 7.x.
|
|
169
|
+
|
|
168
170
|
JRuby, TruffleRuby, and Windows fall back to threads-only mode (no fork) — behaviorally equivalent to stock Sidekiq.
|
|
169
171
|
|
|
170
172
|
## Running the workers
|
|
@@ -279,16 +281,16 @@ There is no native Prometheus `/metrics` endpoint — the StatsD export or the `
|
|
|
279
281
|
|
|
280
282
|
Infrastructure this basic should be free software. A Rails app shouldn't need a licence key to get reliable fetch, batches, rate limiting, or cron — those are table stakes, not a premium tier, and the free-software tradition is that the best tools belong to everyone who runs them.
|
|
281
283
|
|
|
282
|
-
What has made that hard is maintenance: someone has to be paid to do it. Sidekiq funds a decade of *human* maintenance through its paid tiers, which is an honest trade. Wurk makes a different one — it is maintained **by AI**:
|
|
284
|
+
What has made that hard is maintenance: someone has to be paid to do it. Sidekiq funds a decade of *human* maintenance through its paid tiers, which is an honest trade. Wurk makes a different one — it is maintained **by AI**: every author's issues and pull requests, the team's own included, and dependency updates, are handled end to end by a maintainer agent, and the implementation, parity suite, docs, and benchmarks are written and kept current by AI agents working with the team, which still builds the features and cuts the releases. A fix, a doc update, or a version bump is no longer somebody's week, which is what makes it practical to:
|
|
283
285
|
|
|
284
286
|
- ship the entire Pro + Enterprise surface with no tier, no flag gate, and no license check;
|
|
285
287
|
- keep parity honest mechanically rather than by hand — an independently written parity oracle suite, pinned to a documented Sidekiq revision, plus sidekiq-cron's upstream suite running against Wurk on every push ([docs/idea/14-ecosystem-compat.md](https://github.com/developerz-ai/wurk/blob/main/docs/idea/14-ecosystem-compat.md) tracks the rest);
|
|
286
288
|
- keep adding surface Sidekiq doesn't have — the [Wurk extras](#wurk-extras) above landed as one release;
|
|
287
|
-
- hold ourselves to published numbers instead of adjectives — the
|
|
289
|
+
- hold ourselves to published numbers instead of adjectives — the measured comparison against stock Sidekiq is published [as measured](docs/benchmarks.md), including the unflattering ones, and `rake bench:vs_sidekiq` reproduces it. It is a published measurement, not a per-release CI run.
|
|
288
290
|
|
|
289
291
|
DHH makes the broader version of this argument in [Let the agents democratize open source](https://world.hey.com/dhh/let-the-agents-democratize-open-source-9fd630a9): open source fought for everyone's right to change the software they run, and refusing agent-written code re-erects the gate it spent decades tearing down — "all programmers are equal, but some programmers are more equal than others." His subject is contributions; ours is maintenance, which is the same economics from the other end. The reason a licence key guards batches and cron is not that the code is precious, it's that somebody had to be paid to keep it working. Drop that cost and the tier stops being necessary.
|
|
290
292
|
|
|
291
|
-
What makes it work in practice is that the agents run inside machinery built to check them. The
|
|
293
|
+
What makes it work in practice is that the agents run inside machinery built to check them. The parity oracle above is a merge-blocking gate, not decoration; sidekiq-cron's upstream suite runs against Wurk in CI and reports on PRs, but it is not a required check and never gates a merge; the `rake bench` PR regression check flags deltas past a noise-adjusted threshold as an advisory comment; and the published benchmark numbers are one-off historical measurements, reproduced on demand rather than enforced in CI. The release gate has never once let an unverified gem reach RubyGems. Agents supply the pace, the gates supply the certainty — and when a gate does catch something, the fix is to make that class of mistake structurally impossible rather than to slow the agents down. The release lane derives its own tag from `Wurk::VERSION` so the two can't drift apart; [RELEASE.md](https://github.com/developerz-ai/wurk/blob/main/RELEASE.md) walks through it.
|
|
292
294
|
|
|
293
295
|
Wurk is MIT and stays that way. If what you need is a commercial support contract and a human on the other end of an email, buying that is a perfectly good answer.
|
|
294
296
|
|
|
@@ -321,16 +323,16 @@ TypeScript end to end, MIT, and as measured about its own claims as this file is
|
|
|
321
323
|
|
|
322
324
|
## AI-maintained
|
|
323
325
|
|
|
324
|
-
**Wurk is maintained by AI**: by [developerz.ai](https://developerz.ai), the way a human maintainer looks after an open-source project. The developerz-ai org comes back with new ideas once or twice a year. In between, the project is still looked after:
|
|
326
|
+
**Wurk is fully maintained by AI**: by [developerz.ai](https://developerz.ai), the way a human maintainer looks after an open-source project, for every author, org members included (owner ruling, 2026-10-23). The developerz-ai org comes back with new ideas once or twice a year. In between, the project is still looked after:
|
|
325
327
|
|
|
326
328
|
| Who opens it | What happens |
|
|
327
329
|
|---|---|
|
|
328
330
|
| you, an issue | triaged; asked for a reproduction when one is missing; a real bug is fixed and released |
|
|
329
331
|
| you, a pull request | reviewed (CodeRabbit and the developerz.ai reviewer), driven to green, merged |
|
|
330
332
|
| Dependabot | driven to green and merged |
|
|
331
|
-
| an issue or pull request by anyone with write access (org members, collaborators) |
|
|
333
|
+
| an issue or pull request by anyone with write access (org members, collaborators) | handled like anyone's: triaged, reviewed, driven to green, merged |
|
|
332
334
|
| an outside change to CI, release or publish automation | declined: members maintain the pipeline that publishes the gem |
|
|
333
|
-
| a breaking change |
|
|
335
|
+
| a breaking change | the bot's call, not a human's; a change that breaks the drop-in Sidekiq contract is declined, since a revert cannot take that promise back |
|
|
334
336
|
|
|
335
337
|
Releases follow the work, not a calendar. A weekday checkup asks whether a shipping fix has merged and nobody has released it. When one has, the bot files a release request, its coding fleet opens the `Wurk::VERSION` bump PR, and merging that PR publishes the gem ([RELEASE.md](RELEASE.md)). The bot's policy is committed in [`.dz/maintainer/maintainer.yml`](.dz/maintainer/maintainer.yml).
|
|
336
338
|
|
|
@@ -238,11 +238,23 @@ module Wurk
|
|
|
238
238
|
render json: { lid: params[:lid].to_s, history: ::Wurk::Web::Enterprise::Periodic.history(params[:lid].to_s) }
|
|
239
239
|
end
|
|
240
240
|
|
|
241
|
+
# `?minutes=` (default 60) is served raw — the Query layer routes ≤480 to
|
|
242
|
+
# per-minute buckets and >480 onto the 10-minute buckets, raising
|
|
243
|
+
# WindowTooWide (400 below) past the 30-day ceiling; a non-positive window
|
|
244
|
+
# raises ArgumentError there and 400s the same way. `?hours=` is clamped
|
|
245
|
+
# to MAX_HOURS (default 24 when present) and always takes the 10-minute
|
|
246
|
+
# path. The response echoes the REQUESTED window so the SPA's label
|
|
247
|
+
# matches the served period.
|
|
241
248
|
def metrics
|
|
242
|
-
minutes =
|
|
243
|
-
rows = ::Wurk::Web::Enterprise::Historical.top(minutes: minutes, class_filter: params[:substr])
|
|
244
|
-
|
|
245
|
-
|
|
249
|
+
minutes, hours = metrics_window_raw(params)
|
|
250
|
+
rows = ::Wurk::Web::Enterprise::Historical.top(minutes: minutes, hours: hours, class_filter: params[:substr])
|
|
251
|
+
payload = { minutes: hours ? hours * 60 : minutes,
|
|
252
|
+
top_jobs: rows.map { |(klass, totals)| ::Wurk::Api::Serializers.metric_row(klass, totals) } }
|
|
253
|
+
# hours is echoed only when the request used it, so a minutes request
|
|
254
|
+
# keeps the two-key shape (minutes + top_jobs) it has always had.
|
|
255
|
+
payload[:hours] = hours if hours
|
|
256
|
+
render json: payload
|
|
257
|
+
rescue ::ArgumentError, ::Wurk::Metrics::Query::WindowTooWide => e
|
|
246
258
|
render json: { error: e.message }, status: :bad_request
|
|
247
259
|
end
|
|
248
260
|
|
|
@@ -387,8 +399,10 @@ module Wurk
|
|
|
387
399
|
|
|
388
400
|
keys = Array(params[:keys]).map(&:to_s).uniq
|
|
389
401
|
# Each key costs a ZRANGEBYSCORE plus per-entry mutations; an uncapped
|
|
390
|
-
# list lets one POST issue tens of thousands of round-trips. The UI
|
|
391
|
-
#
|
|
402
|
+
# list lets one POST issue tens of thousands of round-trips. The UI's
|
|
403
|
+
# per-row selections accumulate across pages (toggleAll works against
|
|
404
|
+
# the visible page's keys only) and the bulk POST sends the whole Set,
|
|
405
|
+
# so this cap — not the page size — is the bound.
|
|
392
406
|
return render(json: { error: 'too many keys (max 1000)' }, status: :bad_request) if keys.size > 1000
|
|
393
407
|
|
|
394
408
|
entries = entries_for(set, keys)
|
|
@@ -515,12 +529,37 @@ module Wurk
|
|
|
515
529
|
Integer(match[1]) * HISTORY_WINDOW_UNITS.fetch(match[2].empty? ? 's' : match[2])
|
|
516
530
|
end
|
|
517
531
|
|
|
532
|
+
# `?hours=` wins over `?minutes=`; minutes > MAX_MINUTES (480) promote onto
|
|
533
|
+
# the 10-minute buckets in the Query layer, so the clamp ceiling is the
|
|
534
|
+
# 30-day equivalent (MAX_HOURS * 60 minutes), not 480. Absent both, the
|
|
535
|
+
# window defaults to 60 minutes.
|
|
518
536
|
def metrics_window(params)
|
|
519
537
|
pagination = ::Wurk::Api::Pagination
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
538
|
+
max_hours = ::Wurk::Metrics::Query::MAX_HOURS
|
|
539
|
+
if params[:hours]
|
|
540
|
+
[nil, pagination.clamp_int(params[:hours], 1, max_hours, 24)]
|
|
541
|
+
else
|
|
542
|
+
[pagination.clamp_int(params[:minutes], 1, max_hours * 60, 60), nil]
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# #metrics variant: `?minutes=` goes through raw so a window past the
|
|
547
|
+
# 30-day ceiling (MAX_HOURS * 60) reaches the Query layer and raises
|
|
548
|
+
# WindowTooWide — the endpoint answers 400 rather than silently serving
|
|
549
|
+
# a shorter window than the SPA's label claims. Unparseable values fall
|
|
550
|
+
# back to the 60-minute default; `?hours=` is clamped as above.
|
|
551
|
+
def metrics_window_raw(params)
|
|
552
|
+
pagination = ::Wurk::Api::Pagination
|
|
553
|
+
max_hours = ::Wurk::Metrics::Query::MAX_HOURS
|
|
554
|
+
if params[:hours]
|
|
555
|
+
[nil, pagination.clamp_int(params[:hours], 1, max_hours, 24)]
|
|
556
|
+
elsif params[:minutes]
|
|
557
|
+
[Integer(params[:minutes], 10), nil]
|
|
558
|
+
else
|
|
559
|
+
[60, nil]
|
|
560
|
+
end
|
|
561
|
+
rescue ::ArgumentError, ::TypeError
|
|
562
|
+
[60, nil]
|
|
524
563
|
end
|
|
525
564
|
end
|
|
526
565
|
end
|
data/config/routes.rb
CHANGED
|
@@ -16,7 +16,7 @@ Wurk::Engine.routes.draw do
|
|
|
16
16
|
post 'queues/:name/pause', to: 'api#pause_queue', as: :api_pause_queue, constraints: { name: %r{[^/]+} }
|
|
17
17
|
post 'queues/:name/unpause', to: 'api#unpause_queue', as: :api_unpause_queue, constraints: { name: %r{[^/]+} }
|
|
18
18
|
|
|
19
|
-
# Job-set mutations (retry/delete/kill/
|
|
19
|
+
# Job-set mutations (retry/delete/kill/add_to_queue/clear). The SPA posts to
|
|
20
20
|
# these; the Authorization middleware 403s every non-GET in read-only mode,
|
|
21
21
|
# so they need no extra guard. `:key` is "<score>|<jid>" (Wurk::SortedEntry#id);
|
|
22
22
|
# `all/:cmd` operates over the whole set; the bare collection POST is a bulk
|
|
@@ -50,10 +50,10 @@ Wurk::Engine.routes.draw do
|
|
|
50
50
|
post 'dead/:key', to: 'api#dead_job', as: :api_dead_job, constraints: { key: %r{[^/]+} }
|
|
51
51
|
get 'processes', to: 'api#processes'
|
|
52
52
|
get 'workers', to: 'api#workers'
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
53
|
+
# Wurk uses split POST /api/busy/quiet and /api/busy/stop requests, not free
|
|
54
|
+
# spec §25.4 POST /busy with identity/quiet/stop params. Request-shape parity
|
|
55
|
+
# is unsupported; see docs/idea/parity-divergences.md#busy-control-http-request-shape-parity-is-unsupported.
|
|
56
|
+
# Read-only mode 403s these via the Authorization middleware.
|
|
57
57
|
post 'busy/quiet', to: 'api#quiet_process', as: :api_quiet_process
|
|
58
58
|
post 'busy/stop', to: 'api#stop_process', as: :api_stop_process
|
|
59
59
|
# Flows (Wurk::Flow) read the way the batches below them do — a paged index
|
|
@@ -96,13 +96,21 @@ Wurk::Engine.routes.draw do
|
|
|
96
96
|
# version reaches the app and comes back as the same `unsupported_api_version`
|
|
97
97
|
# problem document the other two mounts answer with.
|
|
98
98
|
#
|
|
99
|
-
# Nested here it also inherits
|
|
100
|
-
# `Wurk::Web.use` chain
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
99
|
+
# Nested here it also inherits both engine middlewares — the host's
|
|
100
|
+
# `Wurk::Web.use` chain, then `Wurk::Web::Authorization`
|
|
101
|
+
# (lib/wurk/engine.rb) — so a dashboard behind Devise keeps gating this path
|
|
102
|
+
# too. The `Wurk::Web.configure` authorization hook (403 on falsey,
|
|
103
|
+
# sidekiq-ent §9.2) and the read-only gate both live in Authorization, and
|
|
104
|
+
# the hook runs before the machine-API handoff (lib/wurk/web/config.rb):
|
|
105
|
+
# the host hook gates `/v1` paths too, and a valid machine bearer token does
|
|
106
|
+
# NOT bypass it — bearer auth lives only inside `Wurk::API::App#handle`
|
|
107
|
+
# (lib/wurk/api/app.rb), downstream of both middlewares, so a session-bound
|
|
108
|
+
# hook (`env['warden']&.user`) 403s before the token is ever consulted. A
|
|
109
|
+
# machine client that cannot pass a browser login wants mode 2 instead:
|
|
110
|
+
# `mount Wurk::API => '/wurk-api'` in the host's own routes, declared BEFORE
|
|
111
|
+
# the engine when its path extends the engine's — Rails matches a mounted app
|
|
112
|
+
# by bare string prefix, so `mount Wurk::Engine => '/wurk'` declared first
|
|
113
|
+
# swallows every `/wurk-api/...` request.
|
|
106
114
|
mount Wurk::API => Wurk::API::ENGINE_MOUNT, constraints: ->(request) { Wurk::API.serves?(request.path_info) }
|
|
107
115
|
|
|
108
116
|
# Profiles (v8.0+) — not under /api: `:key/data` streams the gzipped gecko
|
|
@@ -32,8 +32,14 @@ begin
|
|
|
32
32
|
|
|
33
33
|
callback = -> { @@stopping = true } # rubocop:disable Style/ClassVars
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
Wurk.
|
|
35
|
+
# Registered on the configuration directly, not through
|
|
36
|
+
# Wurk.configure_client / Wurk.configure_server: one of the two gates
|
|
37
|
+
# always passes (configure_client while server? is false,
|
|
38
|
+
# configure_server once it is true), so the hook registered either way.
|
|
39
|
+
# Going through a gate would spend the one-time skip warning on Wurk's
|
|
40
|
+
# own internal call and log a spurious WARN — the warning the host's
|
|
41
|
+
# own skipped initializer needs.
|
|
42
|
+
Wurk.configuration.on(:quiet, &callback)
|
|
37
43
|
|
|
38
44
|
# Defer enqueue until the surrounding DB transaction commits so we
|
|
39
45
|
# never push a job whose row hasn't landed. Matches Sidekiq.
|
data/lib/sidekiq/rails.rb
CHANGED
|
@@ -6,5 +6,10 @@
|
|
|
6
6
|
# alone, no ActionDispatch — so this maps to the railtie, NOT the dashboard
|
|
7
7
|
# engine (a wurk-native extra; `require "wurk/rails"` for that). Sidekiq::Web
|
|
8
8
|
# stays mountable as a plain Rack app either way, exactly like upstream.
|
|
9
|
+
# A first require without ActiveJob preloaded needs ActiveSupport's core
|
|
10
|
+
# extensions before rails/railtie. Requiring rails for that bootstrap would
|
|
11
|
+
# also load ActionDispatch, breaking the railtie-only integration boundary.
|
|
9
12
|
require 'wurk'
|
|
13
|
+
require 'active_support'
|
|
14
|
+
require 'active_support/core_ext'
|
|
10
15
|
require 'wurk/railtie'
|
data/lib/wurk/api/read_only.rb
CHANGED
|
@@ -52,7 +52,7 @@ module Wurk
|
|
|
52
52
|
Problem.render(
|
|
53
53
|
Problem::READ_ONLY,
|
|
54
54
|
status: 403,
|
|
55
|
-
detail:
|
|
55
|
+
detail: "This Wurk deployment is read-only; it answers #{SAFE_METHODS.join(', ')} only.",
|
|
56
56
|
instance: request.path
|
|
57
57
|
)
|
|
58
58
|
end
|
|
@@ -41,6 +41,29 @@ module Wurk
|
|
|
41
41
|
|
|
42
42
|
CALLBACK_JOB = 'Wurk::Batch::CallbackJob'
|
|
43
43
|
|
|
44
|
+
# The middleware this one must stay OUTSIDE of — the ordering anchor
|
|
45
|
+
# `Chain#add` consults before appending.
|
|
46
|
+
#
|
|
47
|
+
# Expiry SKIPS an expired job by returning without yielding: inside the
|
|
48
|
+
# batch onion the batch's `ack_success` never runs, the batch's pending
|
|
49
|
+
# never drops and the batch never completes. The Limiter turns an
|
|
50
|
+
# over-limit job into a `Limiter::Rescheduled` (a JobRetry::Skip): inside
|
|
51
|
+
# the batch onion the batch's generic `rescue StandardError` books a
|
|
52
|
+
# spurious failure for a job that never ran.
|
|
53
|
+
#
|
|
54
|
+
# A plain `add` appends, so re-registering this middleware (a
|
|
55
|
+
# `configure_server` block, a test helper) would move it inside both and
|
|
56
|
+
# break the batch's accounting. The anchor keeps it outside.
|
|
57
|
+
#
|
|
58
|
+
# Resolved at call time, not at load time: Expiry is loaded after this
|
|
59
|
+
# file (lib/wurk.rb), so naming the constant here would raise NameError.
|
|
60
|
+
def self.outer_of
|
|
61
|
+
anchors = []
|
|
62
|
+
anchors << Wurk::Middleware::Expiry if defined?(Wurk::Middleware::Expiry)
|
|
63
|
+
anchors << Wurk::Limiter::ServerMiddleware if defined?(Wurk::Limiter::ServerMiddleware)
|
|
64
|
+
anchors
|
|
65
|
+
end
|
|
66
|
+
|
|
44
67
|
def call(_worker, job, _queue, &)
|
|
45
68
|
bid = job['bid']
|
|
46
69
|
return yield unless bid
|
|
@@ -108,4 +131,8 @@ module Wurk
|
|
|
108
131
|
end
|
|
109
132
|
end
|
|
110
133
|
|
|
134
|
+
# Auto-registered on the global server chain. The `outer_of` anchor above
|
|
135
|
+
# keeps this middleware outside Expiry and the Limiter wherever it is
|
|
136
|
+
# registered again later; this registration itself appends as usual, because
|
|
137
|
+
# neither anchor is registered in the chain yet.
|
|
111
138
|
Wurk.configuration.server_middleware.add(Wurk::Batch::ServerMiddleware)
|
data/lib/wurk/capsule.rb
CHANGED
|
@@ -40,6 +40,8 @@ module Wurk
|
|
|
40
40
|
@pools = {}
|
|
41
41
|
@client_chain = nil
|
|
42
42
|
@server_chain = nil
|
|
43
|
+
@server_middleware_seen = []
|
|
44
|
+
@prepared = false
|
|
43
45
|
@watchdog = nil
|
|
44
46
|
end
|
|
45
47
|
|
|
@@ -102,10 +104,38 @@ module Wurk
|
|
|
102
104
|
# hook opened across the fork. Idempotent.
|
|
103
105
|
def prepare_shared!
|
|
104
106
|
client_middleware
|
|
105
|
-
|
|
107
|
+
refresh_server_middleware!
|
|
108
|
+
# A second boot (a launcher run twice) finds the capsule frozen — the
|
|
109
|
+
# flag is already set, so it is not written again.
|
|
110
|
+
@prepared ||= true
|
|
106
111
|
self
|
|
107
112
|
end
|
|
108
113
|
|
|
114
|
+
# True once this capsule has been prepared to run jobs — `prepare_shared!`
|
|
115
|
+
# in the swarm parent before the fork, `prepare!` in the child at boot.
|
|
116
|
+
# Only then is the memoized chain the one the workers run, and only then
|
|
117
|
+
# is it final: a middleware registered on the Configuration afterwards can
|
|
118
|
+
# never reach it.
|
|
119
|
+
def prepared?
|
|
120
|
+
@prepared
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# True when this capsule's server middleware chain has already been
|
|
124
|
+
# snapshotted and that snapshot does not carry `klass`.
|
|
125
|
+
#
|
|
126
|
+
# The snapshot is the copy `server_middleware` memoized; a middleware
|
|
127
|
+
# registered on the Configuration after that copy was taken can never
|
|
128
|
+
# reach this capsule — which is what Wurk::Encryption.enable has to
|
|
129
|
+
# detect before it seals arguments the workers could not open.
|
|
130
|
+
#
|
|
131
|
+
# Pure read: it never materializes the chain and has no side effects.
|
|
132
|
+
def server_middleware_missing?(klass)
|
|
133
|
+
chain = @server_chain
|
|
134
|
+
return false unless chain
|
|
135
|
+
|
|
136
|
+
!chain.exists?(klass)
|
|
137
|
+
end
|
|
138
|
+
|
|
109
139
|
def client_middleware
|
|
110
140
|
chain = (@client_chain ||= @config.client_middleware.copy_for(self))
|
|
111
141
|
yield chain if block_given?
|
|
@@ -116,7 +146,11 @@ module Wurk
|
|
|
116
146
|
# copy_for(self) — not dup — binds the chain's `@config` to this capsule,
|
|
117
147
|
# so middleware that reach for `redis_pool`/`redis`/`logger` resolve them
|
|
118
148
|
# instead of hitting `nil` (a plain dup leaves @config nil).
|
|
119
|
-
chain = (@server_chain ||=
|
|
149
|
+
chain = (@server_chain ||= begin
|
|
150
|
+
config_chain = @config.server_middleware
|
|
151
|
+
@server_middleware_seen = config_chain.entries.map(&:klass)
|
|
152
|
+
config_chain.copy_for(self)
|
|
153
|
+
end)
|
|
120
154
|
yield chain if block_given?
|
|
121
155
|
chain
|
|
122
156
|
end
|
|
@@ -187,6 +221,27 @@ module Wurk
|
|
|
187
221
|
|
|
188
222
|
private
|
|
189
223
|
|
|
224
|
+
# The Configuration's chain may have grown since this capsule copied it —
|
|
225
|
+
# a middleware registered after the copy was taken would otherwise be
|
|
226
|
+
# missing from the chain the workers run. Track classes already seen rather
|
|
227
|
+
# than just those still present, so capsule-local removals stay removed.
|
|
228
|
+
def refresh_server_middleware!
|
|
229
|
+
copy = server_middleware
|
|
230
|
+
return copy if frozen?
|
|
231
|
+
|
|
232
|
+
@config.server_middleware.entries.each do |entry|
|
|
233
|
+
next if @server_middleware_seen.include?(entry.klass)
|
|
234
|
+
|
|
235
|
+
@server_middleware_seen << entry.klass
|
|
236
|
+
next if copy.exists?(entry.klass)
|
|
237
|
+
|
|
238
|
+
rebound = entry.dup
|
|
239
|
+
rebound.config = self
|
|
240
|
+
copy.entries << rebound
|
|
241
|
+
end
|
|
242
|
+
copy
|
|
243
|
+
end
|
|
244
|
+
|
|
190
245
|
# Drop-in fetch pluggability (spec §5 / §4.1): instantiate
|
|
191
246
|
# `config[:fetch_class]` when the host set one — a custom or Pro fetcher —
|
|
192
247
|
# else the default reliable BLMOVE fetcher (`Sidekiq::BasicFetch`). A
|
data/lib/wurk/cli.rb
CHANGED
|
@@ -138,8 +138,10 @@ module Wurk
|
|
|
138
138
|
# Mark server mode BEFORE the app loads so `configure_server` blocks in
|
|
139
139
|
# the required initializer actually fire (they gate on `config.server?`).
|
|
140
140
|
# Matches Sidekiq, which sets `Sidekiq.server = true` before requiring
|
|
141
|
-
# the app in its CLI. Skipping this
|
|
142
|
-
#
|
|
141
|
+
# the app in its CLI. Skipping this drops server middleware, error
|
|
142
|
+
# handlers, and lifecycle hooks registered via configure_server, and each
|
|
143
|
+
# skip logs one WARN naming the gate (`configure_server` / `server? is
|
|
144
|
+
# false`).
|
|
143
145
|
enter_server_mode
|
|
144
146
|
boot_application if boot_app
|
|
145
147
|
self_read, self_write = ::IO.pipe
|
data/lib/wurk/client.rb
CHANGED
|
@@ -87,17 +87,17 @@ module Wurk
|
|
|
87
87
|
payload['jid']
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
-
# @param items [Hash] keys: class, args (Array<Array>), at?, spread_interval?, batch_size?, jid?
|
|
90
|
+
# @param items [Hash] String keys only: class, args (Array<Array>), at?, spread_interval?, batch_size?, jid?
|
|
91
91
|
# @return [Array<String, nil>] jids in submission order; nil entries mark middleware-halted jobs.
|
|
92
92
|
def push_bulk(items)
|
|
93
|
-
args = items['args']
|
|
93
|
+
args = items['args']
|
|
94
94
|
validate_bulk_shape!(items, args)
|
|
95
95
|
return [] if args.empty?
|
|
96
96
|
|
|
97
97
|
base = bulk_base(items)
|
|
98
98
|
reject_bulk_collapse!(base)
|
|
99
99
|
at_values = expand_at(items, args.size)
|
|
100
|
-
batch_sz = items['batch_size'] ||
|
|
100
|
+
batch_sz = items['batch_size'] || (at_values ? SCHEDULED_BATCH_SIZE : DEFAULT_BATCH_SIZE)
|
|
101
101
|
flush_bulk(args, at_values, base, batch_sz)
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -183,14 +183,19 @@ module Wurk
|
|
|
183
183
|
end
|
|
184
184
|
|
|
185
185
|
def validate_bulk_shape!(items, args)
|
|
186
|
+
raise ArgumentError, "Bulk items must be a Hash with 'class' and 'args' keys: `#{items}`" unless items.is_a?(Hash)
|
|
187
|
+
|
|
188
|
+
symbol_keys = items.keys.grep(Symbol)
|
|
189
|
+
# Symbol keys raise here exactly like #push does (spec §31.1) and are
|
|
190
|
+
# never stringified, so every read below can rely on String keys only.
|
|
191
|
+
raise ArgumentError, "Bulk push keys must be Strings: `#{symbol_keys.inspect}`" unless symbol_keys.empty?
|
|
186
192
|
raise ArgumentError, "Bulk arguments must be an Array of Arrays: `#{args.inspect}`" unless valid_bulk_args?(args)
|
|
187
193
|
raise ArgumentError, "Job 'jid' is only allowed with a single-job bulk" if explicit_jid?(items) && args.size > 1
|
|
188
194
|
raise ArgumentError, "Cannot pass both 'at' and 'spread_interval'" if conflicting_schedule?(items)
|
|
189
195
|
end
|
|
190
196
|
|
|
191
197
|
def conflicting_schedule?(items)
|
|
192
|
-
|
|
193
|
-
(items.key?('spread_interval') || items.key?(:spread_interval))
|
|
198
|
+
items.key?('at') && items.key?('spread_interval')
|
|
194
199
|
end
|
|
195
200
|
|
|
196
201
|
def valid_bulk_args?(args)
|
|
@@ -198,11 +203,11 @@ module Wurk
|
|
|
198
203
|
end
|
|
199
204
|
|
|
200
205
|
def explicit_jid?(items)
|
|
201
|
-
items.key?('jid')
|
|
206
|
+
items.key?('jid')
|
|
202
207
|
end
|
|
203
208
|
|
|
204
209
|
def bulk_base(items)
|
|
205
|
-
base = items.
|
|
210
|
+
base = items.dup
|
|
206
211
|
%w[args at spread_interval batch_size].each { |k| base.delete(k) }
|
|
207
212
|
base
|
|
208
213
|
end
|
|
@@ -246,9 +251,9 @@ module Wurk
|
|
|
246
251
|
end
|
|
247
252
|
|
|
248
253
|
def expand_at(items, count)
|
|
249
|
-
return expand_spread(items, count) unless items.key?('at')
|
|
254
|
+
return expand_spread(items, count) unless items.key?('at')
|
|
250
255
|
|
|
251
|
-
at = items['at']
|
|
256
|
+
at = items['at']
|
|
252
257
|
case at
|
|
253
258
|
when Array
|
|
254
259
|
raise ArgumentError, "'at' array size must match args" unless at.size == count
|
|
@@ -263,9 +268,9 @@ module Wurk
|
|
|
263
268
|
end
|
|
264
269
|
|
|
265
270
|
def expand_spread(items, count)
|
|
266
|
-
return nil unless items.key?('spread_interval')
|
|
271
|
+
return nil unless items.key?('spread_interval')
|
|
267
272
|
|
|
268
|
-
spread = items['spread_interval']
|
|
273
|
+
spread = items['spread_interval']
|
|
269
274
|
raise ArgumentError, "'spread_interval' must be positive Numeric" unless spread.is_a?(Numeric) && spread.positive?
|
|
270
275
|
|
|
271
276
|
window = [spread.to_f, SPREAD_INTERVAL_FLOOR].max
|
data/lib/wurk/compat.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Sidekiq aliases. This is the drop-in contract — every public Wurk::*
|
|
4
|
-
# class is exposed under its Sidekiq::* name
|
|
4
|
+
# class is exposed under its Sidekiq::* name except two deliberate
|
|
5
|
+
# exclusions, `Sidekiq::Cron` and `Sidekiq::Status` (#204): those namespaces
|
|
6
|
+
# belong to the third-party sidekiq-cron / sidekiq-status gems, not to
|
|
7
|
+
# Sidekiq itself. Never break the rest.
|
|
5
8
|
#
|
|
6
9
|
# Spec: docs/target/sidekiq-{free,pro,ent}.md.
|
|
7
10
|
|
|
@@ -14,7 +17,14 @@ require_relative 'loader'
|
|
|
14
17
|
# {Wurk.configure_server}; `Sidekiq::Batch`, `Sidekiq::Limiter`,
|
|
15
18
|
# `Sidekiq::Client`, `Sidekiq::Pro::Web`, `Sidekiq::Enterprise` all resolve to
|
|
16
19
|
# their Wurk implementations. This alias surface is the drop-in contract and is
|
|
17
|
-
# never broken.
|
|
20
|
+
# never broken. Two names are deliberately NOT aliased (#204): `Sidekiq::Cron`
|
|
21
|
+
# and `Sidekiq::Status` — those namespaces belong to the third-party
|
|
22
|
+
# sidekiq-cron / sidekiq-status gems. `sidekiq-cron` owns `Sidekiq::Cron`, and
|
|
23
|
+
# squatting it made the gem's own Poller/Job classes collide at load;
|
|
24
|
+
# `sidekiq-status` opens `module Sidekiq::Status`, which resolves the constant
|
|
25
|
+
# instead of defining it, so the alias would hand the gem {Wurk::Status} to
|
|
26
|
+
# reopen and clobber. The guard comments at `CronParser` and `Status` below
|
|
27
|
+
# carry the full rationale.
|
|
18
28
|
#
|
|
19
29
|
# @example The same code targets Sidekiq or Wurk unchanged
|
|
20
30
|
# class MyJob
|