async-job-processor-redis 0.1.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '095272ad3206a878ec049e641b3632181be1f62ca055398230e14dc3fd242d14'
4
- data.tar.gz: b10ab3013a961aca3cbecaa8f51265f4aa8a20aa3eb43031b6a94079d23553f8
3
+ metadata.gz: 5dd229182e417f662b414408d5b49a3c789791f63d7178d2d3acd4c30345b097
4
+ data.tar.gz: 2f28fd28ebbe51ea344810a039aa2985f68bf4b3934240dabb5d5949e9ffa06c
5
5
  SHA512:
6
- metadata.gz: '082e58c7f2f95eff36c022f68ce14a0a1b00e01463bd4f3397a94422b4a4f9011b106d09b21462ce5aeab8dd711af85e8168d1c4423357137fc564720ecb997f'
7
- data.tar.gz: e3bbfd7290feda5b88c941fef9bd4e7ea191c8a800dd97f330f3d1c7032efb869aa73d1a08419b957a541fb78e3d482f6efb816e327c659fd1c26c433cd22479
6
+ metadata.gz: 8818c38d052f06d17c8ada64d537ac996c3bb62bfbbb11c6965e7efa55f4c08b25f19ac9d50c44799b0dd3adcc7b8592ab573cf9793e0fab4500a09c3cfe560a
7
+ data.tar.gz: 9f8a5d182e4f9e7fff41bd01a328a6c50a1349a6dc9136e5fb1566d549e5558e624ccb9442d56e15a32779b5f6b0de4c67b2da619ebc6d8815f58e9183636cc0
checksums.yaml.gz.sig CHANGED
Binary file
@@ -7,6 +7,9 @@ module Async
7
7
  module Job
8
8
  module Processor
9
9
  module Redis
10
+ # Manages delayed job scheduling using Redis sorted sets.
11
+ # Jobs are stored with their execution timestamps and automatically moved
12
+ # to the ready queue when their scheduled time arrives.
10
13
  class DelayedJobs
11
14
  ADD = <<~LUA
12
15
  redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
@@ -22,6 +25,9 @@ module Async
22
25
  return #jobs
23
26
  LUA
24
27
 
28
+ # Initialize a new delayed jobs manager.
29
+ # @parameter client [Async::Redis::Client] The Redis client instance.
30
+ # @parameter key [String] The Redis key for the delayed jobs sorted set.
25
31
  def initialize(client, key)
26
32
  @client = client
27
33
  @key = key
@@ -30,6 +36,16 @@ module Async
30
36
  @move = @client.script(:load, MOVE)
31
37
  end
32
38
 
39
+ # @returns [Integer] The number of jobs currently in the delayed queue.
40
+ def size
41
+ @client.zcard(@key)
42
+ end
43
+
44
+ # Start the background task that moves ready delayed jobs to the ready queue.
45
+ # @parameter ready_list [ReadyList] The ready list to move jobs to.
46
+ # @parameter resolution [Integer] The check interval in seconds.
47
+ # @parameter parent [Async::Task] The parent task to run the background loop in.
48
+ # @returns [Async::Task] The background processing task.
33
49
  def start(ready_list, resolution: 10, parent: Async::Task.current)
34
50
  parent.async do
35
51
  while true
@@ -44,8 +60,14 @@ module Async
44
60
  end
45
61
  end
46
62
 
63
+ # @attribute [String] The Redis key for this delayed jobs queue.
47
64
  attr :key
48
65
 
66
+ # Add a job to the delayed queue with a specified execution time.
67
+ # @parameter job [String] The serialized job data.
68
+ # @parameter timestamp [Time] When the job should be executed.
69
+ # @parameter job_store [JobStore] The job store to save the job data.
70
+ # @returns [String] The unique job ID.
49
71
  def add(job, timestamp, job_store)
50
72
  id = SecureRandom.uuid
51
73
 
@@ -54,7 +76,11 @@ module Async
54
76
  return id
55
77
  end
56
78
 
57
- def move(destination:, now: Time.now.to_i)
79
+ # Move jobs that are ready to be processed from the delayed queue to the destination.
80
+ # @parameter destination [String] The Redis key of the destination queue.
81
+ # @parameter now [Integer] The current timestamp to check against.
82
+ # @returns [Integer] The number of jobs moved.
83
+ def move(destination:, now: Time.now.to_f)
58
84
  @client.evalsha(@move, 2, @key, destination, now)
59
85
  end
60
86
  end
@@ -7,14 +7,23 @@ module Async
7
7
  module Job
8
8
  module Processor
9
9
  module Redis
10
+ # Stores job data using Redis hashes.
11
+ # Provides persistent storage for job payloads indexed by job ID.
10
12
  class JobStore
13
+ # Initialize a new job store.
14
+ # @parameter client [Async::Redis::Client] The Redis client instance.
15
+ # @parameter key [String] The Redis key for the job data hash.
11
16
  def initialize(client, key)
12
17
  @client = client
13
18
  @key = key
14
19
  end
15
20
 
21
+ # @attribute [String] The Redis key for this job store.
16
22
  attr :key
17
23
 
24
+ # Retrieve job data by ID.
25
+ # @parameter id [String] The job ID to retrieve.
26
+ # @returns [String, nil] The serialized job data, or nil if not found.
18
27
  def get(id)
19
28
  @client.hget(@key, id)
20
29
  end
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
6
  module Async
7
7
  module Job
8
8
  module Processor
9
9
  module Redis
10
+ # Manages jobs currently being processed and handles abandoned job recovery.
11
+ # Maintains heartbeats for active workers and automatically requeues
12
+ # jobs from workers that have stopped responding.
10
13
  class ProcessingList
11
14
  REQUEUE = <<~LUA
12
15
  local cursor = "0"
@@ -50,6 +53,12 @@ module Async
50
53
  redis.call('HDEL', KEYS[2], ARGV[1])
51
54
  LUA
52
55
 
56
+ # Initialize a new processing list manager.
57
+ # @parameter client [Async::Redis::Client] The Redis client instance.
58
+ # @parameter key [String] The base Redis key for processing data.
59
+ # @parameter id [String] The unique server/worker ID.
60
+ # @parameter ready_list [ReadyList] The ready job queue.
61
+ # @parameter job_store [JobStore] The job data store.
53
62
  def initialize(client, key, id, ready_list, job_store)
54
63
  @client = client
55
64
  @key = key
@@ -64,36 +73,78 @@ module Async
64
73
  @requeue = @client.script(:load, REQUEUE)
65
74
  @retry = @client.script(:load, RETRY)
66
75
  @complete = @client.script(:load, COMPLETE)
76
+
77
+ @complete_count = 0
67
78
  end
68
79
 
80
+ # @attribute [String] The base Redis key for this processing list.
69
81
  attr :key
70
82
 
83
+ # @attribute [String] The Redis key for this worker's heartbeat.
84
+ attr :heartbeat_key
85
+
86
+ # @attribute [Integer] The total count of all jobs completed by this worker.
87
+ attr :complete_count
88
+
89
+ # @returns [Integer] The number of jobs currently being processed by this worker.
90
+ def size
91
+ @client.llen(@pending_key)
92
+ end
93
+
94
+ # Fetch the next job from the ready queue, moving it to this worker's pending list.
95
+ # This is a blocking operation that waits until a job is available.
96
+ # @returns [String, nil] The job ID, or nil if no job is available.
71
97
  def fetch
72
98
  @client.brpoplpush(@ready_list.key, @pending_key, 0)
73
99
  end
74
100
 
101
+ # Mark a job as completed, removing it from the pending list and job store.
102
+ # @parameter id [String] The job ID to complete.
75
103
  def complete(id)
104
+ @complete_count += 1
105
+
76
106
  @client.evalsha(@complete, 2, @pending_key, @job_store.key, id)
77
107
  end
78
108
 
109
+ # Retry a failed job by moving it back to the ready queue.
110
+ # @parameter id [String] The job ID to retry.
79
111
  def retry(id)
80
112
  Console.warn(self, "Retrying job: #{id}")
81
113
  @client.evalsha(@retry, 2, @pending_key, @ready_list.key, id)
82
114
  end
83
115
 
116
+ # Update heartbeat and requeue any abandoned jobs from inactive workers.
117
+ # @parameter start_time [Float] The start time for calculating uptime.
118
+ # @parameter delay [Numeric] The heartbeat update interval.
119
+ # @parameter factor [Numeric] The heartbeat expiration factor.
120
+ # @returns [Integer] The number of jobs requeued from abandoned workers.
121
+ def requeue(start_time, delay, factor)
122
+ uptime = (Time.now.to_f - start_time).round(2)
123
+ expiry = (delay*factor).ceil
124
+ @client.set(@heartbeat_key, JSON.dump(uptime: uptime), seconds: expiry)
125
+
126
+ # Requeue any jobs that have been abandoned:
127
+ count = @client.evalsha(@requeue, 2, @key, @ready_list.key)
128
+
129
+ return count
130
+ end
131
+
132
+ # Start the background heartbeat and abandoned job recovery task.
133
+ # @parameter delay [Integer] The heartbeat update interval in seconds.
134
+ # @parameter factor [Integer] The heartbeat expiration factor.
135
+ # @parameter parent [Async::Task] The parent task to run the background loop in.
136
+ # @returns [Async::Task] The background processing task.
84
137
  def start(delay: 5, factor: 2, parent: Async::Task.current)
85
- heartbeat_key = "#{@key}:#{@id}"
86
138
  start_time = Time.now.to_f
87
139
 
88
- parent.async do
140
+ parent.async do |task|
89
141
  while true
90
- uptime = (Time.now.to_f - start_time).round(2)
91
- @client.set(heartbeat_key, JSON.dump(uptime: uptime), seconds: delay*factor)
92
-
93
- # Requeue any jobs that have been abandoned:
94
- count = @client.evalsha(@requeue, 2, @key, @ready_list.key)
95
- if count > 0
96
- Console.warn(self, "Requeued #{count} abandoned jobs.")
142
+ task.defer_stop do
143
+ count = self.requeue(start_time, delay, factor)
144
+
145
+ if count > 0
146
+ Console.warn(self, "Requeued #{count} abandoned jobs.")
147
+ end
97
148
  end
98
149
 
99
150
  sleep(delay)
@@ -7,12 +7,17 @@ module Async
7
7
  module Job
8
8
  module Processor
9
9
  module Redis
10
+ # Manages the queue of jobs ready for immediate processing.
11
+ # Jobs are stored in Redis lists with FIFO (first-in, first-out) ordering.
10
12
  class ReadyList
11
13
  ADD = <<~LUA
12
14
  redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
13
15
  redis.call('LPUSH', KEYS[2], ARGV[1])
14
16
  LUA
15
17
 
18
+ # Initialize a new ready list manager.
19
+ # @parameter client [Async::Redis::Client] The Redis client instance.
20
+ # @parameter key [String] The Redis key for the ready job list.
16
21
  def initialize(client, key)
17
22
  @client = client
18
23
  @key = key
@@ -20,8 +25,18 @@ module Async
20
25
  @add = @client.script(:load, ADD)
21
26
  end
22
27
 
28
+ # @attribute [String] The Redis key for this ready list.
23
29
  attr :key
24
30
 
31
+ # @returns [Integer] The number of jobs currently in the ready list.
32
+ def size
33
+ @client.llen(@key)
34
+ end
35
+
36
+ # Add a new job to the ready queue.
37
+ # @parameter job [String] The serialized job data.
38
+ # @parameter job_store [JobStore] The job store to save the job data.
39
+ # @returns [String] The unique job ID.
25
40
  def add(job, job_store)
26
41
  id = SecureRandom.uuid
27
42
 
@@ -1,25 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Released under the MIT License.
4
- # Copyright, 2024, by Samuel Williams.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
5
 
6
- require 'async/idler'
7
- require 'async/job/coder'
8
- require 'async/job/processor/generic'
6
+ require "async/idler"
7
+ require "async/job/coder"
8
+ require "async/job/processor/generic"
9
9
 
10
- require 'securerandom'
10
+ require "securerandom"
11
11
 
12
- require_relative 'delayed_jobs'
13
- require_relative 'job_store'
14
- require_relative 'processing_list'
15
- require_relative 'ready_list'
12
+ require_relative "delayed_jobs"
13
+ require_relative "job_store"
14
+ require_relative "processing_list"
15
+ require_relative "ready_list"
16
16
 
17
17
  module Async
18
18
  module Job
19
19
  module Processor
20
20
  module Redis
21
+ # Redis-backed job processor server.
22
+ # Manages job queues using Redis for distributed job processing across multiple workers.
23
+ # Handles immediate jobs, delayed jobs, and job retry/recovery mechanisms.
21
24
  class Server < Generic
22
- def initialize(delegate, client, prefix: 'async-job', coder: Coder::DEFAULT, resolution: 10, parent: nil)
25
+ # Initialize a new Redis job processor server.
26
+ # @parameter delegate [Object] The delegate object that will process jobs.
27
+ # @parameter client [Async::Redis::Client] The Redis client instance.
28
+ # @parameter prefix [String] The Redis key prefix for job data.
29
+ # @parameter coder [Async::Job::Coder] The job serialization codec.
30
+ # @parameter resolution [Integer] The resolution in seconds for delayed job processing.
31
+ # @parameter parent [Async::Task] The parent task for background processing.
32
+ def initialize(delegate, client, prefix: "async-job", coder: Coder::DEFAULT, resolution: 10, parent: nil)
23
33
  super(delegate)
24
34
 
25
35
  @id = SecureRandom.uuid
@@ -36,6 +46,8 @@ module Async
36
46
  @parent = parent || Async::Idler.new
37
47
  end
38
48
 
49
+ # Start the job processing loop immediately.
50
+ # @returns [Async::Task | false] The processing task or false if already started.
39
51
  def start!
40
52
  return false if @task
41
53
 
@@ -52,6 +64,8 @@ module Async
52
64
  end
53
65
  end
54
66
 
67
+ # Start the server and all background processing tasks.
68
+ # Initializes delayed job processing, abandoned job recovery, and the main processing loop.
55
69
  def start
56
70
  super
57
71
 
@@ -64,12 +78,31 @@ module Async
64
78
  self.start!
65
79
  end
66
80
 
81
+ # Stop the server and all background processing tasks.
67
82
  def stop
68
83
  @task&.stop
69
84
 
70
85
  super
71
86
  end
72
87
 
88
+ # Generates a human-readable string representing the current statistics.
89
+ #
90
+ # e.g. `R=3.42K D=1.23K P=7/2.34K``
91
+ #
92
+ # This can be interpreted as:
93
+ #
94
+ # - R: Number of jobs in the ready list
95
+ # - D: Number of jobs in the delayed queue
96
+ # - P: Number of jobs currently being processed / total number of completed jobs.
97
+ #
98
+ # @returns [String] A string representing the current statistics.
99
+ def status_string
100
+ "R=#{format_count(@ready_list.size)} D=#{format_count(@delayed_jobs.size)} P=#{format_count(@processing_list.size)}/#{format_count(@processing_list.complete_count)}"
101
+ end
102
+
103
+ # Submit a new job for processing.
104
+ # Jobs with a scheduled_at time are queued for delayed processing, while immediate jobs are added to the ready queue.
105
+ # @parameter job [Hash] The job data to process.
73
106
  def call(job)
74
107
  scheduled_at = Coder::Time(job["scheduled_at"])
75
108
 
@@ -97,12 +130,24 @@ module Async
97
130
  @delegate.call(job)
98
131
  @processing_list.complete(id)
99
132
  rescue => error
100
- Console::Event::Failure.for(error).emit(self, "Job failed with error!", id: id)
133
+ Console.error(self, "Job failed with error!", id: id, exception: error)
101
134
  @processing_list.retry(id)
102
135
  end
103
136
  ensure
104
137
  @processing_list.retry(_id) if _id
105
138
  end
139
+
140
+ private
141
+
142
+ def format_count(value)
143
+ if value > 1_000_000
144
+ "#{(value/1_000_000.0).round(2)}M"
145
+ elsif value > 1_000
146
+ "#{(value/1_000.0).round(2)}K"
147
+ else
148
+ value
149
+ end
150
+ end
106
151
  end
107
152
  end
108
153
  end
@@ -7,7 +7,7 @@ module Async
7
7
  module Job
8
8
  module Processor
9
9
  module Redis
10
- VERSION = "0.1.0"
10
+ VERSION = "0.3.0"
11
11
  end
12
12
  end
13
13
  end
@@ -3,13 +3,23 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2024, by Samuel Williams.
5
5
 
6
- require_relative 'redis/server'
7
- require 'async/redis/client'
6
+ require_relative "redis/server"
7
+ require "async/redis/client"
8
8
 
9
+ # @namespace
9
10
  module Async
11
+ # @namespace
10
12
  module Job
13
+ # @namespace
11
14
  module Processor
15
+ # Redis-based job processor implementation.
16
+ # Provides distributed job processing capabilities using Redis as the backend.
12
17
  module Redis
18
+ # Create a new Redis job processor server.
19
+ # @parameter delegate [Object] The delegate object that will process jobs.
20
+ # @parameter endpoint [Async::Redis::Endpoint] The Redis endpoint to connect to.
21
+ # @parameter options [Hash] Additional options passed to the server.
22
+ # @returns [Server] A new Redis job processor server instance.
13
23
  def self.new(delegate, endpoint: Async::Redis.local_endpoint, **options)
14
24
  client = Async::Redis::Client.new(endpoint)
15
25
  return Server.new(delegate, client, **options)
data/license.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2024, by Samuel Williams.
3
+ Copyright, 2024-2025, by Samuel Williams.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/readme.md CHANGED
@@ -12,8 +12,26 @@ Please see the [project documentation](https://socketry.github.io/async-job-proc
12
12
 
13
13
  - [Redis Queue](https://socketry.github.io/async-job-processor-redis/guides/redis-queue/index) - This guide gives a brief overview of the implementation of the Redis queue.
14
14
 
15
+ ## Releases
16
+
17
+ Please see the [project releases](https://socketry.github.io/async-job-processor-redis/releases/index) for all releases.
18
+
19
+ ### v0.3.0
20
+
21
+ - Add `Async::Job::Processor::Redis::Server#status_string` method to return a string with the current job counts.
22
+
23
+ ### v0.2.0
24
+
25
+ - Achieve 100% documentation coverage.
26
+ - Achieve 100% test coverage.
27
+
28
+ ### v0.1.0
29
+
30
+ - Initial release of async-job-processor-redis, migrated from the async-job project.
31
+
15
32
  ## See Also
16
33
 
34
+ - [async-job](https://github.com/socketry/async-job) - Asynchronous job processing framework.
17
35
  - [async-job-adapter-active\_job](https://github.com/socketry/async-job-adapter-active_job) - ActiveJob adapter for `async-job`.
18
36
 
19
37
  ## Contributing
data/releases.md ADDED
@@ -0,0 +1,14 @@
1
+ # Releases
2
+
3
+ ## v0.3.0
4
+
5
+ - Add `Async::Job::Processor::Redis::Server#status_string` method to return a string with the current job counts.
6
+
7
+ ## v0.2.0
8
+
9
+ - Achieve 100% documentation coverage.
10
+ - Achieve 100% test coverage.
11
+
12
+ ## v0.1.0
13
+
14
+ - Initial release of async-job-processor-redis, migrated from the async-job project.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,11 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: async-job-processor-redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain:
11
10
  - |
@@ -37,7 +36,7 @@ cert_chain:
37
36
  Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
37
  voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
38
  -----END CERTIFICATE-----
40
- date: 2024-08-14 00:00:00.000000000 Z
39
+ date: 1980-01-02 00:00:00.000000000 Z
41
40
  dependencies:
42
41
  - !ruby/object:Gem::Dependency
43
42
  name: async-job
@@ -67,8 +66,6 @@ dependencies:
67
66
  - - ">="
68
67
  - !ruby/object:Gem::Version
69
68
  version: '0'
70
- description:
71
- email:
72
69
  executables: []
73
70
  extensions: []
74
71
  extra_rdoc_files: []
@@ -82,13 +79,12 @@ files:
82
79
  - lib/async/job/processor/redis/version.rb
83
80
  - license.md
84
81
  - readme.md
85
- homepage:
82
+ - releases.md
86
83
  licenses:
87
84
  - MIT
88
85
  metadata:
89
86
  documentation_uri: https://socketry.github.io/async-job-processor-redis/
90
87
  source_code_uri: https://github.com/socketry/async-job-processor-redis
91
- post_install_message:
92
88
  rdoc_options: []
93
89
  require_paths:
94
90
  - lib
@@ -96,15 +92,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
92
  requirements:
97
93
  - - ">="
98
94
  - !ruby/object:Gem::Version
99
- version: '3.1'
95
+ version: '3.2'
100
96
  required_rubygems_version: !ruby/object:Gem::Requirement
101
97
  requirements:
102
98
  - - ">="
103
99
  - !ruby/object:Gem::Version
104
100
  version: '0'
105
101
  requirements: []
106
- rubygems_version: 3.5.11
107
- signing_key:
102
+ rubygems_version: 3.6.7
108
103
  specification_version: 4
109
104
  summary: A asynchronous job queue for Ruby.
110
105
  test_files: []
metadata.gz.sig CHANGED
Binary file