wurk 1.0.3 → 1.0.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 000bd71bc3b9148d1f4250d32ebe25e300beee9f384b013ed3efabf2f778b1ce
4
- data.tar.gz: eec8ea9f45d0ef0ccc0b894515dd6711adf08054757e0475178b45ed8ec81c37
3
+ metadata.gz: 4f6df1c551958881248c0b6a97709e2a4999ec57f976cac37371d7c3e1c5d4e7
4
+ data.tar.gz: 450a316b29bada67e8deac2f1411ec1b084f57a2af48e752800b5aed01c9a0af
5
5
  SHA512:
6
- metadata.gz: 7b173cdc536f04367d2eea9b5c549d71f6fe7cbfa27095af3916dc690196421e45356bd663f35a1777a7b340ad57fe6c5b6be24aec3eb57aea3d128de30944a9
7
- data.tar.gz: f89b858d93982f20efa5a1d79b8b2230736cf53bdd8b3d95d54fcc4c444bb90dc70822124d8ae80060b2110a68aec8116f807759bccc87b2478cd61872fa890a
6
+ metadata.gz: 077cad32a71630367889448188e4bcaa59ed16b5853f37caf01efa5f907c939a4937d04fccb8df21eb256d9b685a50c2dce01cd041e5c5870573ee4c77eb7ef5
7
+ data.tar.gz: ec16e8f19b58332974d0e38cdb5ecb665b508698465435de29014ce2ff395ff1b374a827b098bcdd22abf1db9dc36d4692c3900e6b8091c1c22b0ebb1be878d1
data/README.md CHANGED
@@ -56,6 +56,7 @@ Plus Wurk extras: a worker topology DSL, a Kubernetes liveness/readiness listene
56
56
  ## Documentation
57
57
 
58
58
  - **[Website](https://developerz-ai.github.io/wurk/)** · **[Wiki / full docs](https://github.com/developerz-ai/wurk/wiki)** — the pitch, install, and the complete guide.
59
+ - **[API reference (YARD)](https://developerz-ai.github.io/wurk/api/)** — generated docs for the public classes (`Wurk::Worker`, `Wurk::Client`, `Wurk::Configuration`, `Wurk::Batch`, `Wurk::Limiter`, `Wurk::Unique`, and the `Sidekiq::*` aliases). Machine-readable map for AI agents: **[llms.txt](https://developerz-ai.github.io/wurk/llms.txt)**.
59
60
  - **[Getting started & architecture](https://github.com/developerz-ai/wurk/blob/main/docs/idea/01-overview.md)** — how the swarm, manager, fetcher, and processor fit together.
60
61
  - **[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.
61
62
  - **[Active Job adapter](https://github.com/developerz-ai/wurk/blob/main/docs/active-job.md)** — run `ActiveJob`/`deliver_later` on Wurk with `queue_adapter = :wurk`.
@@ -16,9 +16,10 @@ Wurk.configure_server do |config|
16
16
  # Seconds to let in-flight jobs finish on shutdown (Sidekiq-compatible key):
17
17
  # config[:timeout] = 25
18
18
 
19
- # The default is a single worker process (fork). To run several, declare a
20
- # topology `flat` spawns N identical forks; use `slot`s for dedicated
21
- # queues. See docs/idea/03-process-model.md.
19
+ # By default Wurk forks one worker process per CPU core (set WURK_COUNT, or
20
+ # SIDEKIQ_COUNT, to override). For full control declare a topology — `flat`
21
+ # spawns N identical forks; use `slot`s for dedicated queues. Total in-flight
22
+ # jobs = forks × concurrency. See docs/idea/03-process-model.md.
22
23
  # config.topology = Wurk::Topology.flat(count: 2, queues: %w[critical default low], concurrency: 10)
23
24
  end
24
25
 
data/lib/wurk/batch.rb CHANGED
@@ -9,6 +9,15 @@ module Wurk
9
9
  # Sidekiq Pro Batches. Group jobs, attach success/complete/death callbacks,
10
10
  # track progress. Spec: docs/target/sidekiq-pro.md §2.
11
11
  #
12
+ # @example Define a batch with a success callback
13
+ # batch = Sidekiq::Batch.new
14
+ # batch.description = "Nightly import"
15
+ # batch.on(:success, ImportCallback, "user_id" => user.id)
16
+ # batch.jobs do
17
+ # rows.each { |r| ImportRowJob.perform_async(r.id) }
18
+ # end
19
+ # batch.bid # => the batch id, persisted in Redis
20
+ #
12
21
  # Lifecycle:
13
22
  # 1. `Batch.new` allocates a fresh BID; the batch is `mutable?` until the
14
23
  # first `#jobs` block flushes — that flush HSETs the core hash, ZADDs
data/lib/wurk/capsule.rb CHANGED
@@ -38,7 +38,7 @@ module Wurk
38
38
  # %w[high default low] → mode :strict, weights all 0
39
39
  # %w[high,3 default,2 low,1] → mode :weighted, weights {q=>w}
40
40
  # %w[a,1 b,1 c,1] → mode :random, weights all 1
41
- # @queues is expanded by weight so a uniform shuffle gives weighted
41
+ # `@queues` is expanded by weight so a uniform shuffle gives weighted
42
42
  # fairness (e.g. ["high","high","high","default","default","low"]).
43
43
  def queues=(val)
44
44
  parsed = Array(val).map { |entry| parse_queue_entry(entry) }
data/lib/wurk/cli.rb CHANGED
@@ -398,10 +398,22 @@ module Wurk
398
398
 
399
399
  def boot_rails_application(require_path)
400
400
  require 'rails'
401
+ define_active_job_adapter
401
402
  require ::File.expand_path("#{require_path}/config/environment.rb")
402
403
  @config[:tag] ||= default_tag(::Rails.root) if defined?(::Rails) && ::Rails.respond_to?(:root)
403
404
  end
404
405
 
406
+ # Standalone mode never loads the engine, so the ActiveJob `:wurk` /
407
+ # `:sidekiq` adapters it normally defines are absent. Define them BEFORE the
408
+ # app boots — an app with `config.active_job.queue_adapter = :wurk` resolves
409
+ # the adapter during environment boot, which otherwise raises `uninitialized
410
+ # constant WurkAdapter` (#253). `require` no-ops for a mounted-engine app
411
+ # that already loaded it, and the adapter file itself rescues a missing
412
+ # ActiveJob gem, so a non-ActiveJob app is unaffected.
413
+ def define_active_job_adapter
414
+ require_relative '../active_job/queue_adapters/wurk_adapter'
415
+ end
416
+
405
417
  def initialize_logger
406
418
  return unless @config[:verbose] || ENV['DEBUG_INVOCATION'] == '1'
407
419
 
data/lib/wurk/client.rb CHANGED
@@ -7,7 +7,14 @@ require_relative 'lua'
7
7
  module Wurk
8
8
  # Enqueue interface. Pipelined LPUSH / ZADD writes against the canonical
9
9
  # Sidekiq Redis schema — never change keys, JSON shape, or score format here:
10
- # wire-compat is sacred.
10
+ # wire-compat is sacred. Most apps enqueue through the {Wurk::Worker} DSL
11
+ # (`MyJob.perform_async`), which routes here; reach for Client directly only to
12
+ # push a raw job hash or to drive the bulk/scheduled path explicitly.
13
+ #
14
+ # @example Push a raw job hash
15
+ # Wurk::Client.new.push("class" => "MyJob", "args" => [1, 2], "queue" => "default")
16
+ # @example Bulk enqueue in one round-trip
17
+ # Wurk::Client.new.push_bulk("class" => "MyJob", "args" => [[1], [2], [3]])
11
18
  #
12
19
  # Spec: docs/target/sidekiq-free.md §7.
13
20
  class Client
@@ -36,6 +43,7 @@ module Wurk
36
43
  copy
37
44
  end
38
45
 
46
+ # @param item [Hash] job payload; must carry `class` and `args`, may carry `at`, `queue`, `jid`, etc.
39
47
  # @return [String, nil] jid; nil when client middleware halts the push.
40
48
  def push(item)
41
49
  normed = normalize_item(item)
data/lib/wurk/compat.rb CHANGED
@@ -5,6 +5,25 @@
5
5
  #
6
6
  # Spec: docs/target/sidekiq-{free,pro,ent}.md.
7
7
 
8
+ # The `Sidekiq::*` compatibility namespace. Every public Wurk class is exposed
9
+ # here under its Sidekiq name so an existing Sidekiq/Pro/Enterprise app runs on
10
+ # Wurk after a one-line gem swap. `Sidekiq::Job` / `Sidekiq::Worker` are
11
+ # {Wurk::Job} / {Wurk::Worker}; `Sidekiq.configure_server` is
12
+ # {Wurk.configure_server}; `Sidekiq::Batch`, `Sidekiq::Limiter`,
13
+ # `Sidekiq::Client`, `Sidekiq::Pro::Web`, `Sidekiq::Enterprise` all resolve to
14
+ # their Wurk implementations. This alias surface is the drop-in contract and is
15
+ # never broken.
16
+ #
17
+ # @example The same code targets Sidekiq or Wurk unchanged
18
+ # class MyJob
19
+ # include Sidekiq::Job
20
+ # def perform(id); end
21
+ # end
22
+ # Sidekiq.configure_server { |c| c.concurrency = 10 }
23
+ #
24
+ # @note `Sidekiq.pro?` and `Sidekiq.ent?` return `false` — Wurk ships the Pro/Ent
25
+ # features for free but advertises as OSS. Don't gate behavior on them.
26
+ # @see https://github.com/developerz-ai/wurk/blob/main/docs/migrate-from-sidekiq.md Migration guide
8
27
  module Sidekiq
9
28
  # Version stamps mirror Sidekiq's OSS release Wurk targets for compat.
10
29
  # Third-party gems version-gate on these; raise the MAJOR only when the
@@ -112,8 +112,14 @@ module Wurk
112
112
 
113
113
  # --- Default capsule shortcuts ---------------------------------------
114
114
 
115
+ # @return [Integer] threads per worker process for the default capsule
116
+ # (default 5). The *process* count is separate — set via `WURK_COUNT`
117
+ # (defaults to the CPU count). With a single capsule, total in-flight
118
+ # jobs = `WURK_COUNT × concurrency`; with multiple capsules see
119
+ # {#total_concurrency} for the cluster aggregate.
115
120
  def concurrency = default_capsule.concurrency
116
121
 
122
+ # @param val [Integer] threads per worker process
117
123
  def concurrency=(val)
118
124
  default_capsule.concurrency = val
119
125
  end
@@ -251,7 +257,18 @@ module Wurk
251
257
 
252
258
  # Yields a Wurk::Cron::Manager so the host app can register periodic
253
259
  # jobs at boot. Manager state is shared per-process so multiple
254
- # `config.periodic` blocks accumulate (matches Sidekiq Ent §2.1).
260
+ # `config.periodic` blocks accumulate (matches Sidekiq Ent §2.1). This is
261
+ # the native replacement for the `sidekiq-cron` gem.
262
+ #
263
+ # @example Register cron jobs at boot
264
+ # Wurk.configure_server do |config|
265
+ # config.periodic do |mgr|
266
+ # mgr.register("*/5 * * * *", ReportJob)
267
+ # mgr.register("0 0 * * *", NightlyJob, tz: "UTC")
268
+ # end
269
+ # end
270
+ # @yieldparam mgr [Wurk::Cron::Manager] call `mgr.register(cron, JobClass, **opts)`
271
+ # @return [Wurk::Cron::Manager]
255
272
  #
256
273
  # Spec: docs/target/sidekiq-ent.md §2.
257
274
  def periodic
@@ -96,7 +96,7 @@ module Wurk
96
96
 
97
97
  # Prefixed queue keys (`queue:<name>`) in fetch order. Strict mode
98
98
  # preserves declaration order. Random/weighted shuffle each call —
99
- # @queues is pre-expanded by weight in Capsule#queues=, so uniform
99
+ # `@queues` is pre-expanded by weight in Capsule#queues=, so uniform
100
100
  # shuffle yields weighted fairness; .uniq trims duplicates. Paused
101
101
  # queues are filtered after shuffle so the membership test runs on
102
102
  # the smallest possible set.
data/lib/wurk/launcher.rb CHANGED
@@ -241,10 +241,10 @@ module Wurk
241
241
  end
242
242
 
243
243
  # Heartbeat thread loop. `safe_thread` already wraps exceptions. Loops on
244
- # @stopped — NOT @done — so a *quieted* process keeps beating and publishes
244
+ # `@stopped` — NOT `@done` — so a *quieted* process keeps beating and publishes
245
245
  # `quiet=true` instead of vanishing from the live set (#236). Only `#stop`
246
- # flips @stopped; its `Thread#wakeup` breaks the sleep so the loop re-checks
247
- # @stopped and exits without waiting out the interval.
246
+ # flips `@stopped`; its `Thread#wakeup` breaks the sleep so the loop re-checks
247
+ # `@stopped` and exits without waiting out the interval.
248
248
  def start_heartbeat
249
249
  until @stopped
250
250
  heartbeat
@@ -5,7 +5,7 @@ require_relative 'base'
5
5
  module Wurk
6
6
  module Limiter
7
7
  # Leaky bucket: drain rate = bucket_size / drain ops/sec. Stored as a
8
- # HASH of {level, last} — Lua compares level vs bucket_size after
8
+ # HASH of `{level, last}` — Lua compares level vs bucket_size after
9
9
  # leaking elapsed * drain_per_sec.
10
10
  class Leaky < Base
11
11
  WAIT_SLEEP = 0.05
data/lib/wurk/limiter.rb CHANGED
@@ -11,6 +11,16 @@ module Wurk
11
11
  # clock skew across hosts doesn't matter inside one Redis. Spec:
12
12
  # docs/target/sidekiq-ent.md §1.
13
13
  #
14
+ # @example Throttle to 50 concurrent uses, waiting up to the default timeout
15
+ # STRIPE = Sidekiq::Limiter.concurrent("stripe", 50)
16
+ #
17
+ # class ChargeJob
18
+ # include Sidekiq::Job
19
+ # def perform(id)
20
+ # STRIPE.within_limit { Stripe::Charge.create(...) }
21
+ # end
22
+ # end
23
+ #
14
24
  # Layout (one file per type under `lib/wurk/limiter/`):
15
25
  # * `Limiter::Base` owns the metadata write (lmtr:{name}) + the global
16
26
  # `lmtr-list` registration so the Web UI can list every limiter, and
data/lib/wurk/unique.rb CHANGED
@@ -25,6 +25,22 @@ module Wurk
25
25
  # holding the owning JID. Scheduled jobs extend the TTL by the delay so
26
26
  # the lock covers the entire wait+execution window (§3.4).
27
27
  #
28
+ # This is the native replacement for the `sidekiq-unique-jobs` gem; see the
29
+ # migration guide for the `lock:` → `unique_until:` translation table.
30
+ #
31
+ # @example Enable globally, then declare per worker
32
+ # Sidekiq::Enterprise.unique! # activate the middleware once, at boot
33
+ #
34
+ # class ChargeJob
35
+ # include Sidekiq::Job
36
+ # sidekiq_options unique_for: 10.minutes, unique_until: :success
37
+ #
38
+ # # optional: customize the dedup key
39
+ # def self.sidekiq_unique_context(job)
40
+ # job["args"].first # dedup on the first arg only
41
+ # end
42
+ # end
43
+ #
28
44
  # Spec: docs/target/sidekiq-ent.md §3.
29
45
  module Unique
30
46
  KEY_PREFIX = 'unique:'
@@ -131,7 +147,11 @@ module Wurk
131
147
  # (skip). Returns nil when uniqueness should be skipped.
132
148
  def self.coerce_ttl(value)
133
149
  return nil if value.nil? || value == false
134
- return value if value.is_a?(Integer) && value.positive?
150
+ # `.to_i`, not `value`: ActiveSupport::Duration overrides `is_a?(Integer)`
151
+ # to return true (it delegates to its underlying value), so `1.hour` passes
152
+ # this guard — returning the raw Duration handed redis-client a non-Integer
153
+ # EX arg and raised TypeError at enqueue (#253).
154
+ return value.to_i if value.is_a?(Integer) && value.positive?
135
155
  return value.to_i if value.is_a?(Numeric)
136
156
  return value.to_i if duration_like?(value)
137
157
 
data/lib/wurk/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Wurk
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
5
5
  end
data/lib/wurk/worker.rb CHANGED
@@ -3,9 +3,27 @@
3
3
  require_relative 'worker/setter'
4
4
 
5
5
  module Wurk
6
- # The user-facing DSL: `include Wurk::Worker` (aliased to Sidekiq::Worker).
7
- # Owns `sidekiq_options`, `perform_async`, `perform_in`, `perform_at`,
8
- # `set`, `sidekiq_retry_in`, etc.
6
+ # The user-facing job DSL. `include Wurk::Worker` or its modern alias
7
+ # `Sidekiq::Job` / `Sidekiq::Worker` — onto a class to make it a background
8
+ # job. The class gains `sidekiq_options`, the `perform_*` enqueue methods,
9
+ # `set`, and the retry-hook DSL; each instance gains `jid`, `logger`,
10
+ # `interrupted?`, and the batch helpers.
11
+ #
12
+ # @example A minimal job
13
+ # class HardJob
14
+ # include Sidekiq::Job
15
+ # sidekiq_options queue: "critical", retry: 5
16
+ #
17
+ # def perform(user_id, opts = {})
18
+ # # ... your work ...
19
+ # end
20
+ # end
21
+ #
22
+ # HardJob.perform_async(42, "fast" => true) # enqueue now
23
+ # HardJob.perform_in(5.minutes, 42) # enqueue later
24
+ #
25
+ # @see Wurk::Worker::ClassMethods the enqueue + options DSL added to the class
26
+ # @see https://github.com/developerz-ai/wurk/blob/main/docs/migrate-from-sidekiq.md Migration guide
9
27
  #
10
28
  # Spec: docs/target/sidekiq-free.md §6 (Sidekiq::Job).
11
29
  module Worker
@@ -65,7 +83,17 @@ module Wurk
65
83
  batch.valid?
66
84
  end
67
85
 
86
+ # Class-level DSL mixed into every job class by {Wurk::Worker}. These are
87
+ # the public enqueue and configuration entry points.
68
88
  module ClassMethods # rubocop:disable Metrics/ModuleLength
89
+ # Set per-class job options (merged over any inherited options).
90
+ #
91
+ # @example
92
+ # sidekiq_options queue: "mailers", retry: 3, unique_for: 10.minutes
93
+ # @param opts [Hash] any of `queue:`, `retry:`, `dead:`, `backtrace:`,
94
+ # `expires_in:`, `tags:`, `pool:`, `unique_for:`, … (see the migration
95
+ # guide's sidekiq_options table for the full set)
96
+ # @return [Hash] the merged, string-keyed options hash
69
97
  def sidekiq_options(opts = {})
70
98
  merged = get_sidekiq_options.merge(opts.transform_keys(&:to_s))
71
99
  @sidekiq_options_hash = merged
@@ -92,24 +120,57 @@ module Wurk
92
120
  self.sidekiq_retries_exhausted_block = block
93
121
  end
94
122
 
123
+ # Enqueue the job to run as soon as a worker is free. Arguments are
124
+ # forwarded to `#perform` and must be JSON-serializable
125
+ # (string/number/bool/nil/array/hash).
126
+ #
127
+ # @example
128
+ # EmailJob.perform_async(user.id, "welcome")
129
+ # @return [String, nil] the job id (jid), or nil if a client middleware
130
+ # halted the push
95
131
  def perform_async(*)
96
132
  Wurk::Worker::Setter.new(self, {}).perform_async(*)
97
133
  end
98
134
 
135
+ # Run `#perform` synchronously in the current thread (no Redis). Useful in
136
+ # tests and for inline execution.
137
+ #
138
+ # @return [Object] the return value of `#perform`
99
139
  def perform_inline(*)
100
140
  new.perform(*)
101
141
  end
102
142
  alias perform_sync perform_inline
103
143
 
144
+ # Schedule the job for later. `perform_at` is an alias taking an absolute
145
+ # time; `perform_in` takes a relative interval.
146
+ #
147
+ # @example
148
+ # ReminderJob.perform_in(1.hour, lead.id)
149
+ # ReminderJob.perform_at(Time.now + 3600, lead.id)
150
+ # @param interval [Numeric, Time] seconds-from-now, or an absolute Time
151
+ # @return [String, nil] the job id (jid)
104
152
  def perform_in(interval, *)
105
153
  Wurk::Worker::Setter.new(self, {}).perform_in(interval, *)
106
154
  end
107
155
  alias perform_at perform_in
108
156
 
157
+ # Enqueue many jobs in one round-trip via the Lua bulk path.
158
+ #
159
+ # @example
160
+ # ImportJob.perform_bulk([[1], [2], [3]])
161
+ # @param items [Array<Array>] one args array per job
162
+ # @return [Array<String>] the job ids, in order
109
163
  def perform_bulk(items, **)
110
164
  Wurk::Worker::Setter.new(self, {}).perform_bulk(items, **)
111
165
  end
112
166
 
167
+ # Return a per-call option carrier so a single enqueue can override
168
+ # class-level options (queue, scheduling, pool, …).
169
+ #
170
+ # @example
171
+ # ReportJob.set(queue: "low").perform_async(account.id)
172
+ # @param opts [Hash] per-call overrides
173
+ # @return [Wurk::Worker::Setter]
113
174
  def set(opts)
114
175
  Wurk::Worker::Setter.new(self, opts)
115
176
  end
data/lib/wurk.rb CHANGED
@@ -72,6 +72,18 @@ require_relative 'wurk/deploy'
72
72
 
73
73
  require 'json'
74
74
 
75
+ # Wurk — a 100% API-compatible, free, faster drop-in for Sidekiq + Sidekiq Pro
76
+ # + Sidekiq Enterprise. Same Redis key schema, same job JSON, same Ruby DSL;
77
+ # real parallelism via a fork-based swarm. Every public class is also exposed
78
+ # under its `Sidekiq::*` name (see {Sidekiq}).
79
+ #
80
+ # Start here:
81
+ # * {Wurk::Worker} / `Sidekiq::Job` — define and enqueue jobs.
82
+ # * {Wurk::Configuration} — `Wurk.configure_server { |config| ... }`; note
83
+ # `WURK_COUNT` (forked processes) × `concurrency` (threads) = total in-flight.
84
+ # * {Wurk::Batch}, {Wurk::Limiter}, {Wurk::Unique} — Pro/Enterprise features, free.
85
+ #
86
+ # @see https://github.com/developerz-ai/wurk/blob/main/docs/migrate-from-sidekiq.md Migration guide
75
87
  module Wurk
76
88
  class Error < StandardError; end
77
89