async-job-processor-redis 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '095272ad3206a878ec049e641b3632181be1f62ca055398230e14dc3fd242d14'
4
+ data.tar.gz: b10ab3013a961aca3cbecaa8f51265f4aa8a20aa3eb43031b6a94079d23553f8
5
+ SHA512:
6
+ metadata.gz: '082e58c7f2f95eff36c022f68ce14a0a1b00e01463bd4f3397a94422b4a4f9011b106d09b21462ce5aeab8dd711af85e8168d1c4423357137fc564720ecb997f'
7
+ data.tar.gz: e3bbfd7290feda5b88c941fef9bd4e7ea191c8a800dd97f330f3d1c7032efb869aa73d1a08419b957a541fb78e3d482f6efb816e327c659fd1c26c433cd22479
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Job
8
+ module Processor
9
+ module Redis
10
+ class DelayedJobs
11
+ ADD = <<~LUA
12
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
13
+ redis.call('ZADD', KEYS[2], ARGV[3], ARGV[1])
14
+ LUA
15
+
16
+ MOVE = <<~LUA
17
+ local jobs = redis.call('ZRANGEBYSCORE', KEYS[1], 0, ARGV[1])
18
+ redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
19
+ if #jobs > 0 then
20
+ redis.call('LPUSH', KEYS[2], unpack(jobs))
21
+ end
22
+ return #jobs
23
+ LUA
24
+
25
+ def initialize(client, key)
26
+ @client = client
27
+ @key = key
28
+
29
+ @add = @client.script(:load, ADD)
30
+ @move = @client.script(:load, MOVE)
31
+ end
32
+
33
+ def start(ready_list, resolution: 10, parent: Async::Task.current)
34
+ parent.async do
35
+ while true
36
+ count = move(destination: ready_list.key)
37
+
38
+ if count > 0
39
+ Console.debug(self, "Moved #{count} delayed jobs to ready list.")
40
+ end
41
+
42
+ sleep(resolution)
43
+ end
44
+ end
45
+ end
46
+
47
+ attr :key
48
+
49
+ def add(job, timestamp, job_store)
50
+ id = SecureRandom.uuid
51
+
52
+ @client.evalsha(@add, 2, job_store.key, @key, id, job, timestamp.to_f)
53
+
54
+ return id
55
+ end
56
+
57
+ def move(destination:, now: Time.now.to_i)
58
+ @client.evalsha(@move, 2, @key, destination, now)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Job
8
+ module Processor
9
+ module Redis
10
+ class JobStore
11
+ def initialize(client, key)
12
+ @client = client
13
+ @key = key
14
+ end
15
+
16
+ attr :key
17
+
18
+ def get(id)
19
+ @client.hget(@key, id)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Job
8
+ module Processor
9
+ module Redis
10
+ class ProcessingList
11
+ REQUEUE = <<~LUA
12
+ local cursor = "0"
13
+ local count = 0
14
+
15
+ repeat
16
+ -- Scan through all known server id -> job id mappings and requeue any jobs that have been abandoned:
17
+ local result = redis.call('SCAN', cursor, 'MATCH', KEYS[1]..':*:pending')
18
+ cursor = result[1]
19
+ for _, pending_key in pairs(result[2]) do
20
+ -- Check if the server is still active:
21
+ local server_key = KEYS[1]..":"..pending_key:match("([^:]+):pending")
22
+ local state = redis.call('GET', server_key)
23
+ if state == false then
24
+ while true do
25
+ -- Requeue any pending jobs:
26
+ local result = redis.call('RPOPLPUSH', pending_key, KEYS[2])
27
+
28
+ if result == false then
29
+ -- Delete the pending list:
30
+ redis.call('DEL', pending_key)
31
+ break
32
+ end
33
+
34
+ count = count + 1
35
+ end
36
+ end
37
+ end
38
+ until cursor == "0"
39
+
40
+ return count
41
+ LUA
42
+
43
+ RETRY = <<~LUA
44
+ redis.call('LREM', KEYS[1], 1, ARGV[1])
45
+ redis.call('LPUSH', KEYS[2], ARGV[1])
46
+ LUA
47
+
48
+ COMPLETE = <<~LUA
49
+ redis.call('LREM', KEYS[1], 1, ARGV[1])
50
+ redis.call('HDEL', KEYS[2], ARGV[1])
51
+ LUA
52
+
53
+ def initialize(client, key, id, ready_list, job_store)
54
+ @client = client
55
+ @key = key
56
+ @id = id
57
+
58
+ @ready_list = ready_list
59
+ @job_store = job_store
60
+
61
+ @pending_key = "#{@key}:#{@id}:pending"
62
+ @heartbeat_key = "#{@key}:#{@id}"
63
+
64
+ @requeue = @client.script(:load, REQUEUE)
65
+ @retry = @client.script(:load, RETRY)
66
+ @complete = @client.script(:load, COMPLETE)
67
+ end
68
+
69
+ attr :key
70
+
71
+ def fetch
72
+ @client.brpoplpush(@ready_list.key, @pending_key, 0)
73
+ end
74
+
75
+ def complete(id)
76
+ @client.evalsha(@complete, 2, @pending_key, @job_store.key, id)
77
+ end
78
+
79
+ def retry(id)
80
+ Console.warn(self, "Retrying job: #{id}")
81
+ @client.evalsha(@retry, 2, @pending_key, @ready_list.key, id)
82
+ end
83
+
84
+ def start(delay: 5, factor: 2, parent: Async::Task.current)
85
+ heartbeat_key = "#{@key}:#{@id}"
86
+ start_time = Time.now.to_f
87
+
88
+ parent.async do
89
+ 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.")
97
+ end
98
+
99
+ sleep(delay)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Job
8
+ module Processor
9
+ module Redis
10
+ class ReadyList
11
+ ADD = <<~LUA
12
+ redis.call('HSET', KEYS[1], ARGV[1], ARGV[2])
13
+ redis.call('LPUSH', KEYS[2], ARGV[1])
14
+ LUA
15
+
16
+ def initialize(client, key)
17
+ @client = client
18
+ @key = key
19
+
20
+ @add = @client.script(:load, ADD)
21
+ end
22
+
23
+ attr :key
24
+
25
+ def add(job, job_store)
26
+ id = SecureRandom.uuid
27
+
28
+ @client.evalsha(@add, 2, job_store.key, @key, id, job)
29
+
30
+ return id
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require 'async/idler'
7
+ require 'async/job/coder'
8
+ require 'async/job/processor/generic'
9
+
10
+ require 'securerandom'
11
+
12
+ require_relative 'delayed_jobs'
13
+ require_relative 'job_store'
14
+ require_relative 'processing_list'
15
+ require_relative 'ready_list'
16
+
17
+ module Async
18
+ module Job
19
+ module Processor
20
+ module Redis
21
+ class Server < Generic
22
+ def initialize(delegate, client, prefix: 'async-job', coder: Coder::DEFAULT, resolution: 10, parent: nil)
23
+ super(delegate)
24
+
25
+ @id = SecureRandom.uuid
26
+ @client = client
27
+ @prefix = prefix
28
+ @coder = coder
29
+ @resolution = resolution
30
+
31
+ @job_store = JobStore.new(@client, "#{@prefix}:jobs")
32
+ @delayed_jobs = DelayedJobs.new(@client, "#{@prefix}:delayed")
33
+ @ready_list = ReadyList.new(@client, "#{@prefix}:ready")
34
+ @processing_list = ProcessingList.new(@client, "#{@prefix}:processing", @id, @ready_list, @job_store)
35
+
36
+ @parent = parent || Async::Idler.new
37
+ end
38
+
39
+ def start!
40
+ return false if @task
41
+
42
+ @task = true
43
+
44
+ @parent.async(transient: true, annotation: self.class.name) do |task|
45
+ @task = task
46
+
47
+ while true
48
+ self.dequeue(task)
49
+ end
50
+ ensure
51
+ @task = nil
52
+ end
53
+ end
54
+
55
+ def start
56
+ super
57
+
58
+ # Start the delayed processor, which will move jobs to the ready processor when they are ready:
59
+ @delayed_jobs.start(@ready_list, resolution: @resolution)
60
+
61
+ # Start the processing processor, which will move jobs to the ready processor when they are abandoned:
62
+ @processing_list.start
63
+
64
+ self.start!
65
+ end
66
+
67
+ def stop
68
+ @task&.stop
69
+
70
+ super
71
+ end
72
+
73
+ def call(job)
74
+ scheduled_at = Coder::Time(job["scheduled_at"])
75
+
76
+ if scheduled_at
77
+ @delayed_jobs.add(@coder.dump(job), scheduled_at, @job_store)
78
+ else
79
+ @ready_list.add(@coder.dump(job), @job_store)
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ # Dequeue a job from the ready list and process it.
86
+ #
87
+ # If the job fails for any reason, it will be retried.
88
+ #
89
+ # If you do not desire this behavior, you should catch exceptions in the delegate.
90
+ def dequeue(parent)
91
+ _id = @processing_list.fetch
92
+
93
+ parent.async do
94
+ id = _id; _id = nil
95
+
96
+ job = @coder.load(@job_store.get(id))
97
+ @delegate.call(job)
98
+ @processing_list.complete(id)
99
+ rescue => error
100
+ Console::Event::Failure.for(error).emit(self, "Job failed with error!", id: id)
101
+ @processing_list.retry(id)
102
+ end
103
+ ensure
104
+ @processing_list.retry(_id) if _id
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ module Async
7
+ module Job
8
+ module Processor
9
+ module Redis
10
+ VERSION = "0.1.0"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'redis/server'
7
+ require 'async/redis/client'
8
+
9
+ module Async
10
+ module Job
11
+ module Processor
12
+ module Redis
13
+ def self.new(delegate, endpoint: Async::Redis.local_endpoint, **options)
14
+ client = Async::Redis::Client.new(endpoint)
15
+ return Server.new(delegate, client, **options)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
data/license.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright, 2024, by Samuel Williams.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,35 @@
1
+ # Async::Job::Processor::Redis
2
+
3
+ Provides an asynchronous job server.
4
+
5
+ [![Development Status](https://github.com/socketry/async-job-processor-redis/workflows/Test/badge.svg)](https://github.com/socketry/async-job-processor-redis/actions?workflow=Test)
6
+
7
+ ## Usage
8
+
9
+ Please see the [project documentation](https://socketry.github.io/async-job-processor-redis/) for more details.
10
+
11
+ - [Getting Started](https://socketry.github.io/async-job-processor-redis/guides/getting-started/index) - This guide gives you an overview of the `async-job-processor-redis` gem.
12
+
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
+
15
+ ## See Also
16
+
17
+ - [async-job-adapter-active\_job](https://github.com/socketry/async-job-adapter-active_job) - ActiveJob adapter for `async-job`.
18
+
19
+ ## Contributing
20
+
21
+ We welcome contributions to this project.
22
+
23
+ 1. Fork it.
24
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
25
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
26
+ 4. Push to the branch (`git push origin my-new-feature`).
27
+ 5. Create new Pull Request.
28
+
29
+ ### Developer Certificate of Origin
30
+
31
+ In order to protect users of this project, we require all contributors to comply with the [Developer Certificate of Origin](https://developercertificate.org/). This ensures that all contributions are properly licensed and attributed.
32
+
33
+ ### Community Guidelines
34
+
35
+ This project is best served by a collaborative and respectful environment. Treat each other professionally, respect differing viewpoints, and engage constructively. Harassment, discrimination, or harmful behavior is not tolerated. Communicate clearly, listen actively, and support one another. If any issues arise, please inform the project maintainers.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-job-processor-redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Williams
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIE2DCCA0CgAwIBAgIBATANBgkqhkiG9w0BAQsFADBhMRgwFgYDVQQDDA9zYW11
14
+ ZWwud2lsbGlhbXMxHTAbBgoJkiaJk/IsZAEZFg1vcmlvbnRyYW5zZmVyMRIwEAYK
15
+ CZImiZPyLGQBGRYCY28xEjAQBgoJkiaJk/IsZAEZFgJuejAeFw0yMjA4MDYwNDUz
16
+ MjRaFw0zMjA4MDMwNDUzMjRaMGExGDAWBgNVBAMMD3NhbXVlbC53aWxsaWFtczEd
17
+ MBsGCgmSJomT8ixkARkWDW9yaW9udHJhbnNmZXIxEjAQBgoJkiaJk/IsZAEZFgJj
18
+ bzESMBAGCgmSJomT8ixkARkWAm56MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIB
19
+ igKCAYEAomvSopQXQ24+9DBB6I6jxRI2auu3VVb4nOjmmHq7XWM4u3HL+pni63X2
20
+ 9qZdoq9xt7H+RPbwL28LDpDNflYQXoOhoVhQ37Pjn9YDjl8/4/9xa9+NUpl9XDIW
21
+ sGkaOY0eqsQm1pEWkHJr3zn/fxoKPZPfaJOglovdxf7dgsHz67Xgd/ka+Wo1YqoE
22
+ e5AUKRwUuvaUaumAKgPH+4E4oiLXI4T1Ff5Q7xxv6yXvHuYtlMHhYfgNn8iiW8WN
23
+ XibYXPNP7NtieSQqwR/xM6IRSoyXKuS+ZNGDPUUGk8RoiV/xvVN4LrVm9upSc0ss
24
+ RZ6qwOQmXCo/lLcDUxJAgG95cPw//sI00tZan75VgsGzSWAOdjQpFM0l4dxvKwHn
25
+ tUeT3ZsAgt0JnGqNm2Bkz81kG4A2hSyFZTFA8vZGhp+hz+8Q573tAR89y9YJBdYM
26
+ zp0FM4zwMNEUwgfRzv1tEVVUEXmoFCyhzonUUw4nE4CFu/sE3ffhjKcXcY//qiSW
27
+ xm4erY3XAgMBAAGjgZowgZcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0O
28
+ BBYEFO9t7XWuFf2SKLmuijgqR4sGDlRsMC4GA1UdEQQnMCWBI3NhbXVlbC53aWxs
29
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MC4GA1UdEgQnMCWBI3NhbXVlbC53aWxs
30
+ aWFtc0BvcmlvbnRyYW5zZmVyLmNvLm56MA0GCSqGSIb3DQEBCwUAA4IBgQB5sxkE
31
+ cBsSYwK6fYpM+hA5B5yZY2+L0Z+27jF1pWGgbhPH8/FjjBLVn+VFok3CDpRqwXCl
32
+ xCO40JEkKdznNy2avOMra6PFiQyOE74kCtv7P+Fdc+FhgqI5lMon6tt9rNeXmnW/
33
+ c1NaMRdxy999hmRGzUSFjozcCwxpy/LwabxtdXwXgSay4mQ32EDjqR1TixS1+smp
34
+ 8C/NCWgpIfzpHGJsjvmH2wAfKtTTqB9CVKLCWEnCHyCaRVuKkrKjqhYCdmMBqCws
35
+ JkxfQWC+jBVeG9ZtPhQgZpfhvh+6hMhraUYRQ6XGyvBqEUe+yo6DKIT3MtGE2+CP
36
+ eX9i9ZWBydWb8/rvmwmX2kkcBbX0hZS1rcR593hGc61JR6lvkGYQ2MYskBveyaxt
37
+ Q2K9NVun/S785AP05vKkXZEFYxqG6EW012U4oLcFl5MySFajYXRYbuUpH6AY+HP8
38
+ voD0MPg1DssDLKwXyt1eKD/+Fq0bFWhwVM/1XiAXL7lyYUyOq24KHgQ2Csg=
39
+ -----END CERTIFICATE-----
40
+ date: 2024-08-14 00:00:00.000000000 Z
41
+ dependencies:
42
+ - !ruby/object:Gem::Dependency
43
+ name: async-job
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '0.10'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '0.10'
56
+ - !ruby/object:Gem::Dependency
57
+ name: async-redis
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ description:
71
+ email:
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/async/job/processor/redis.rb
77
+ - lib/async/job/processor/redis/delayed_jobs.rb
78
+ - lib/async/job/processor/redis/job_store.rb
79
+ - lib/async/job/processor/redis/processing_list.rb
80
+ - lib/async/job/processor/redis/ready_list.rb
81
+ - lib/async/job/processor/redis/server.rb
82
+ - lib/async/job/processor/redis/version.rb
83
+ - license.md
84
+ - readme.md
85
+ homepage:
86
+ licenses:
87
+ - MIT
88
+ metadata:
89
+ documentation_uri: https://socketry.github.io/async-job-processor-redis/
90
+ source_code_uri: https://github.com/socketry/async-job-processor-redis
91
+ post_install_message:
92
+ rdoc_options: []
93
+ require_paths:
94
+ - lib
95
+ required_ruby_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '3.1'
100
+ required_rubygems_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubygems_version: 3.5.11
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: A asynchronous job queue for Ruby.
110
+ test_files: []
metadata.gz.sig ADDED
Binary file