async-job 0.0.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: 2fa7b9ed6bf57940243caa6fae0e8d72a9e5c6eaf38f13311b800abb80850c09
4
+ data.tar.gz: 9dce5abfa5b06e6e00592e6ca474241ab92806d65b70c94aab0d0763e5196a19
5
+ SHA512:
6
+ metadata.gz: 79557b886d6fadc8ae47644c36916c556091f4eac1c78f5d4b3271163a00fa08d5e68931d867fe7ea1871f6257cbd9b00fd413561ecd6a3a244e101a2d2685bf
7
+ data.tar.gz: 0cbce58e990cff1881f29c4c91f7db93aae228d44d85ecde799b22d88ab5824df5dbabd5c11045dddba01c37d0b644914169adc8c6ed86b38fa7b353492763a7
checksums.yaml.gz.sig ADDED
Binary file
@@ -0,0 +1,54 @@
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 Backend
9
+ module Redis
10
+ class DelayedQueue
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
+ LUA
23
+
24
+ def initialize(client, key)
25
+ @client = client
26
+ @key = key
27
+
28
+ @add = @client.script(:load, ADD)
29
+ @move = @client.script(:load, MOVE)
30
+ end
31
+
32
+ def start(ready_queue, resolution: 10, parent: Async::Task.current)
33
+ parent.async do
34
+ while true
35
+ move(destination: ready_queue.key)
36
+ sleep(resolution)
37
+ end
38
+ end
39
+ end
40
+
41
+ attr :key
42
+
43
+ def add(job, job_store)
44
+ @client.evalsha(@add, 2, job_store.key, @key, job.id, job.serialize, job.perform_at.to_f)
45
+ end
46
+
47
+ def move(destination:, now: Time.now.to_i)
48
+ @client.evalsha(@move, 2, @key, destination, now)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ 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 Backend
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,112 @@
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 Backend
9
+ module Redis
10
+ class ProcessingQueue
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_queue, job_store)
54
+ @client = client
55
+ @key = key
56
+ @id = id
57
+
58
+ @ready_queue = ready_queue
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
+ Console.info(self, "Fetching job...")
73
+ id = @client.brpoplpush(@ready_queue.key, @pending_key, 0)
74
+ Console.info(self, "Fetching job: #{id}")
75
+ return id
76
+ end
77
+
78
+ def complete(id)
79
+ Console.info(self, "Completing job: #{id}")
80
+ @client.evalsha(@complete, 2, @pending_key, @job_store.key, id)
81
+ end
82
+
83
+ def retry(id)
84
+ Console.warn(self, "Retrying job: #{id}")
85
+ @client.evalsha(@retry, 2, @pending_key, @ready_queue.key, id)
86
+ end
87
+
88
+ def start(delay: 5, factor: 2, parent: Async::Task.current)
89
+ heartbeat_key = "#{@key}:#{@id}"
90
+
91
+ start_time = Time.now.to_f
92
+
93
+ parent.async do
94
+ while true
95
+ uptime = (Time.now.to_f - start_time).round(2)
96
+ @client.set(heartbeat_key, JSON.dump(uptime: uptime), seconds: delay*factor)
97
+
98
+ # Requeue any jobs that have been abandoned:
99
+ count = @client.evalsha(@requeue, 2, @key, @ready_queue.key)
100
+ if count > 0
101
+ Console.warn(self, "Requeued #{count} abandoned jobs.")
102
+ end
103
+
104
+ sleep(delay)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,32 @@
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 Backend
9
+ module Redis
10
+ class ReadyQueue
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
+ @client.evalsha(@add, 2, job_store.key, @key, job.id, job.serialize)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'delayed_queue'
7
+ require_relative 'job_store'
8
+ require_relative 'processing_queue'
9
+ require_relative 'ready_queue'
10
+
11
+ require 'securerandom'
12
+
13
+ module Async
14
+ module Job
15
+ module Backend
16
+ module Redis
17
+ class Server
18
+ def initialize(client, prefix)
19
+ @id = SecureRandom.uuid
20
+ @client = client
21
+ @prefix = prefix
22
+
23
+ @job_store = JobStore.new(@client, "#{@prefix}:jobs")
24
+
25
+ @delayed_queue = DelayedQueue.new(@client, "#{@prefix}:delayed")
26
+ @ready_queue = ReadyQueue.new(@client, "#{@prefix}:ready")
27
+
28
+ @processing_queue = ProcessingQueue.new(@client, "#{@prefix}:processing", @id, @ready_queue, @job_store)
29
+ end
30
+
31
+ def start
32
+ # Start the delayed queue, which will move jobs to the ready queue when they are ready:
33
+ @delayed_queue.start(@ready_queue)
34
+
35
+ # Start the processing queue, which will move jobs to the ready queue when they are abandoned:
36
+ @processing_queue.start
37
+ end
38
+
39
+ def enqueue(job)
40
+ if perform_at = job.perform_at and perform_at > Time.now.to_f
41
+ # If the job is delayed, add it to the delayed queue:
42
+ @delayed_queue.add(job, @job_store)
43
+ else
44
+ # If the job is ready to be processed now, add it to the ready queue:
45
+ @ready_queue.add(job, @job_store)
46
+ end
47
+ end
48
+
49
+ def each(&block)
50
+ while id = @processing_queue.fetch
51
+ begin
52
+ job = @job_store.get(id)
53
+ yield id, job
54
+ @processing_queue.complete(id)
55
+ rescue => error
56
+ @processing_queue.retry(id)
57
+ raise
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,30 @@
1
+ module Async
2
+ module Job
3
+ class Generic
4
+ def self.enqueue(...)
5
+ self.new(...).enqueue
6
+
7
+ def initialize(id, perform_at: nil)
8
+ @id = id
9
+ @perform_at = perform_at
10
+ end
11
+
12
+ attr :id
13
+ attr :perform_at
14
+
15
+ def serialize
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def call
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+
27
+ server.enqueue(job)
28
+ -> job.serialize
29
+
30
+ Server.enqueue(job)
@@ -0,0 +1,10 @@
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
+ VERSION = "0.0.0"
9
+ end
10
+ end
data/lib/async/job.rb ADDED
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024, by Samuel Williams.
5
+
6
+ require_relative 'job/version'
7
+ require_relative 'job/backend/redis/server'
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,27 @@
1
+ # Async::Job
2
+
3
+ Provides an asynchronous job server.
4
+
5
+ [![Development Status](https://github.com/socketry/async-job/workflows/Test/badge.svg)](https://github.com/socketry/async-job/actions?workflow=Test)
6
+
7
+ ## Usage
8
+
9
+ The current implementation is incomplete, but with `redis` running on localhost, you can run several instances of `client.rb` and `server.rb` to show the general operation.
10
+
11
+ ## Contributing
12
+
13
+ We welcome contributions to this project.
14
+
15
+ 1. Fork it.
16
+ 2. Create your feature branch (`git checkout -b my-new-feature`).
17
+ 3. Commit your changes (`git commit -am 'Add some feature'`).
18
+ 4. Push to the branch (`git push origin my-new-feature`).
19
+ 5. Create new Pull Request.
20
+
21
+ ### Developer Certificate of Origin
22
+
23
+ This project uses the [Developer Certificate of Origin](https://developercertificate.org/). All contributors to this project must agree to this document to have their contributions accepted.
24
+
25
+ ### Contributor Covenant
26
+
27
+ This project is governed by the [Contributor Covenant](https://www.contributor-covenant.org/). All contributors and participants agree to abide by its terms.
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,110 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async-job
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.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-02-14 00:00:00.000000000 Z
41
+ dependencies:
42
+ - !ruby/object:Gem::Dependency
43
+ name: async
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '1.0'
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '1.0'
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.rb
77
+ - lib/async/job/backend/redis/delayed_queue.rb
78
+ - lib/async/job/backend/redis/job_store.rb
79
+ - lib/async/job/backend/redis/processing_queue.rb
80
+ - lib/async/job/backend/redis/ready_queue.rb
81
+ - lib/async/job/backend/redis/server.rb
82
+ - lib/async/job/generic.rb
83
+ - lib/async/job/version.rb
84
+ - license.md
85
+ - readme.md
86
+ homepage:
87
+ licenses:
88
+ - MIT
89
+ metadata:
90
+ documentation_uri: https://socketry.github.io/async-job
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.0'
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.3
107
+ signing_key:
108
+ specification_version: 4
109
+ summary: A asynchronous job queue for Ruby.
110
+ test_files: []
metadata.gz.sig ADDED
@@ -0,0 +1,3 @@
1
+ m�� �A������>������4̌E�<k��J�V]�];hy%�]��;/Xևu1����@����Y������^A��
2
+ ?~�G���&8���Zw2k�^�74��3 �r(�)���A��}L�Uߺ��$��E�Z�� ;�HO�#X�Q�������D�R�=������ f����M�Nxe�� HT�Ҥ�:�@_����@S5��>��ކ���{�� b>Ph0h]�}�ᑻ��]b鈆��2}̅�� �/h��TXJ���
3
+ |Zt��,l�$t�V��� ���8��&�)�d�38�pPS�U�Xp���^��5�l�b9,JOj�*�j6D�'