tally_jobs 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +136 -0
- data/Rakefile +8 -0
- data/lib/generators/tally_jobs/install_generator.rb +15 -0
- data/lib/generators/templates/tally_jobs.rb +8 -0
- data/lib/tally_jobs/configs.rb +31 -0
- data/lib/tally_jobs/configured_job.rb +16 -0
- data/lib/tally_jobs/counter_store/memory_counter_store.rb +20 -0
- data/lib/tally_jobs/counter_store/redis_counter_store.rb +33 -0
- data/lib/tally_jobs/counter_store.rb +17 -0
- data/lib/tally_jobs/engine.rb +6 -0
- data/lib/tally_jobs/jobs_counter.rb +39 -0
- data/lib/tally_jobs/tally_data.rb +22 -0
- data/lib/tally_jobs/version.rb +5 -0
- data/lib/tally_jobs.rb +70 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3b77ee7cc397f75d41288171fdda4c1b6afcfbd33f492b1a4b020ca32c8dc358
|
4
|
+
data.tar.gz: 44e3bf15cc99abe02cf3024bc1cf598fa6be7982fde5930b84f871eca64fa8f5
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cb8455dcfde48404752169a07c0ebe7b100916ff6dd6455a11f5735f805836d3cd3ea91b6871f2fac51465257c1b5ed857df4212c8b6bd9ad0b676adb31cdfc9
|
7
|
+
data.tar.gz: d2442c12d194153c62a69f0f9408d5fc4832f4f101f9c7685955b3648fa5666376303a9e5e417cb38207ce1a4c4f834202e432bca4cf4303e81dcf300b69a4a6
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# TallyJobs
|
2
|
+
|
3
|
+
Collect all params of the same jobs within an interval time then enqueue that job only one time.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "tally_jobs"
|
9
|
+
|
10
|
+
$ bundle install
|
11
|
+
$ rails g tally_jobs:install
|
12
|
+
```
|
13
|
+
|
14
|
+
## Usage
|
15
|
+
|
16
|
+
Assume that you have a job like this
|
17
|
+
```ruby
|
18
|
+
class ReportSpamCommentJob < ApplicationJob
|
19
|
+
queue_as :default
|
20
|
+
|
21
|
+
def perform(comment_id)
|
22
|
+
comment = Comment.find(comment_id)
|
23
|
+
Report.report_spam_comment(comment) if SpamDetective.check_spam(comment)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class CommentController < ApplicationController
|
28
|
+
# ...
|
29
|
+
def create
|
30
|
+
# ...
|
31
|
+
ReportSpamCommentJob.enqueue_to_tally(comment.id)
|
32
|
+
# ...
|
33
|
+
end
|
34
|
+
|
35
|
+
def update
|
36
|
+
# ...
|
37
|
+
ReportSpamCommentJob.perform_later(comment.id)
|
38
|
+
# ...
|
39
|
+
end
|
40
|
+
# ...
|
41
|
+
end
|
42
|
+
```
|
43
|
+
|
44
|
+
In a peak time, a lot of comments are created and so a lot of jobs are enqueued.
|
45
|
+
If we gather all comment ids within a small interval time then enqueue only one job for them, so what we need is just one read query to fetch all comments, and one write query to report all spam comments.
|
46
|
+
|
47
|
+
`tally_jobs` gem help you to do that:
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class CommentController < ApplicationController
|
51
|
+
# ...
|
52
|
+
def create
|
53
|
+
# ...
|
54
|
+
ReportSpamCommentJob.enqueue_to_tally(comment.id)
|
55
|
+
# ...
|
56
|
+
end
|
57
|
+
# ...
|
58
|
+
end
|
59
|
+
|
60
|
+
class ReportSpamCommentJob < ApplicationJob
|
61
|
+
queue_as :default
|
62
|
+
|
63
|
+
include TallyJobs::TallyData
|
64
|
+
|
65
|
+
def perform(*comment_ids)
|
66
|
+
comments = Comment.where(id: comment_ids) # one read query
|
67
|
+
if spams = SpamDetective.check_spams(comments)
|
68
|
+
Report.report_spam_comments(spams) # one write query
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
The basic idea:
|
75
|
+
|
76
|
+
- you call `YouJob#enqueue_to_tally` to enqueue your job to a jobs-queue (in development, it is a `Thread::Queue`, in production it is `Redis List`).
|
77
|
+
|
78
|
+
- `tally_jobs` will start a counter thread which, every interval time, will pop enqueued jobs, collect params list group by ActiveJob/ConfiguredJob class, then enqueue each job with its params collection.
|
79
|
+
|
80
|
+
|
81
|
+
## Notes
|
82
|
+
|
83
|
+
- `ActiveJob::ConfiguredJob` is counted separately from `ActiveJob`
|
84
|
+
```ruby
|
85
|
+
ReportSpamCommentJob.enqueue_to_tally(1)
|
86
|
+
ReportSpamCommentJob.enqueue_to_tally(2)
|
87
|
+
ReportSpamCommentJob.set(wait_until: Date.tomorrow.noon).enqueue_to_tally(3)
|
88
|
+
ReportSpamCommentJob.set(wait_until: Date.tomorrow.noon).enqueue_to_tally(4)
|
89
|
+
# => enqueue 2 tally job
|
90
|
+
# one for ReportSpamCommentJob with [1,2]
|
91
|
+
# and one for [@job_class=ReportSpamCommentJob, @options={:wait_until=>Thu, 25 Apr 2024 12:00:00.000000000 UTC +00:00}] with [3,4]
|
92
|
+
#
|
93
|
+
```
|
94
|
+
|
95
|
+
- support perform in-batch
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
class ReportSpamCommentJob < ApplicationJob
|
99
|
+
queue_as :default
|
100
|
+
|
101
|
+
include TallyJobs::TallyData
|
102
|
+
|
103
|
+
batch_size 100
|
104
|
+
|
105
|
+
def perform(one_hundred_comment_ids)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
```
|
109
|
+
|
110
|
+
- call `TallyJobs#stop` to stop counter thread, call `TallyJobs#restart` to restart counter thread
|
111
|
+
- in tests, just stop counter thread before all test cases, if you want to test job enqueued, you could start/flush/stop counter thread on each test case:
|
112
|
+
|
113
|
+
```ruby
|
114
|
+
expect {
|
115
|
+
TallyJobs.restart
|
116
|
+
|
117
|
+
ReportSpamCommentJob.enqueue_to_tally(3)
|
118
|
+
ReportSpamCommentJob.enqueue_to_tally(4)
|
119
|
+
ReportSpamCommentJob.enqueue_to_tally(5)
|
120
|
+
TallyJobs.flush # force collect and enqueue jobs
|
121
|
+
# expect ...
|
122
|
+
|
123
|
+
TallyJobs.stop # this is also call flush
|
124
|
+
}.to have_enqueued_job(ReportSpamCommentJob).with([3,4,5])
|
125
|
+
```
|
126
|
+
|
127
|
+
## Todo
|
128
|
+
|
129
|
+
- handling back pressure
|
130
|
+
- support multiple tally jobs queues base on ActiveJob `queue_as`, set `interval-time` for each queue base on it's priority (higher priority, smaller interval time)
|
131
|
+
- support ActionMailer ???
|
132
|
+
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
|
136
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tally_jobs.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails/generators/base'
|
4
|
+
|
5
|
+
module TallyJobs
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("../../templates", __FILE__)
|
9
|
+
|
10
|
+
def copy_initializer
|
11
|
+
copy_file "tally_jobs.rb", "config/initializers/tally_jobs.rb"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
TallyJobs.setup do |config|
|
4
|
+
config.interval = 120 # seconds
|
5
|
+
config.logger = Rails.logger
|
6
|
+
# config.counter_store = :memory_counter_store
|
7
|
+
config.counter_store = { store: :redis_counter_store, redis: Redis.new(host: "localhost") } if Rails.env.production?
|
8
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./counter_store/memory_counter_store"
|
4
|
+
require_relative "./counter_store/redis_counter_store"
|
5
|
+
|
6
|
+
module TallyJobs
|
7
|
+
class Configs
|
8
|
+
attr_accessor :interval, :logger, :redis
|
9
|
+
|
10
|
+
def redis=(_redis)
|
11
|
+
redis = _redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def counter_store=(store)
|
15
|
+
JobsCounter.store = \
|
16
|
+
case store
|
17
|
+
when :memory_counter_store
|
18
|
+
TallyJobs::CounterStore::MemoryCounterStore.new
|
19
|
+
when Hash
|
20
|
+
case store[:store]
|
21
|
+
when :redis_counter_store
|
22
|
+
TallyJobs::CounterStore::RedisCounterStore.new(**store.except(:store))
|
23
|
+
else
|
24
|
+
store.delete(:store).to_s.classify.constantize.new(**store)
|
25
|
+
end
|
26
|
+
else
|
27
|
+
store.to_s.classify.constantize.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TallyJobs
|
4
|
+
module ConfiguredJob
|
5
|
+
def enqueue_to_tally(*params)
|
6
|
+
TallyJobs::JobsCounter.store.enqueue(
|
7
|
+
[self.instance_variable_get(:@job_class), self.instance_variable_get(:@options)],
|
8
|
+
*params
|
9
|
+
)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# to support enqueue_to_tally preconfigured jobs
|
15
|
+
require 'active_job'
|
16
|
+
::ActiveJob::ConfiguredJob.include TallyJobs::ConfiguredJob
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../counter_store.rb"
|
4
|
+
|
5
|
+
module TallyJobs::CounterStore
|
6
|
+
class MemoryCounterStore < Base
|
7
|
+
# in-memory job queue
|
8
|
+
JOBS_QUEUE = Thread::Queue.new
|
9
|
+
|
10
|
+
def enqueue(job_clazz, *params)
|
11
|
+
JOBS_QUEUE.enq([job_clazz, *params])
|
12
|
+
end
|
13
|
+
|
14
|
+
def dequeue(n = 1)
|
15
|
+
JOBS_QUEUE.deq
|
16
|
+
end
|
17
|
+
|
18
|
+
delegate :empty?, :clear, to: "TallyJobs::CounterStore::MemoryCounterStore::JOBS_QUEUE"
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "../counter_store.rb"
|
4
|
+
|
5
|
+
module TallyJobs::CounterStore
|
6
|
+
class RedisCounterStore < Base
|
7
|
+
attr_reader :redis
|
8
|
+
|
9
|
+
def initialize(redis:)
|
10
|
+
@redis = redis
|
11
|
+
end
|
12
|
+
|
13
|
+
def enqueue(job_clazz, *params)
|
14
|
+
@redis.rpush(KEY, Marshal.dump([job_clazz, *params]))
|
15
|
+
end
|
16
|
+
|
17
|
+
def dequeue(n = 1)
|
18
|
+
Marshal.load(@redis.lpop(KEY))
|
19
|
+
end
|
20
|
+
|
21
|
+
def empty?
|
22
|
+
@redis.llen(KEY).zero?
|
23
|
+
end
|
24
|
+
|
25
|
+
def clear
|
26
|
+
@redis.del(KEY)
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
KEY = "tally-jobs-queue".freeze
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "./counter_store/memory_counter_store"
|
4
|
+
|
5
|
+
module TallyJobs
|
6
|
+
class JobsCounter
|
7
|
+
cattr_accessor :store, default: TallyJobs::CounterStore::MemoryCounterStore.new
|
8
|
+
|
9
|
+
def self.collect_then_perform_later
|
10
|
+
groups = Hash.new { |h, k| h[k] = [] }
|
11
|
+
until store.empty?
|
12
|
+
job_clazz, *params = store.dequeue
|
13
|
+
groups[job_clazz] << (params.size == 1 ? params[0] : params)
|
14
|
+
end
|
15
|
+
|
16
|
+
groups.each do |job_clazz, params_list|
|
17
|
+
batch_size = nil
|
18
|
+
|
19
|
+
if job_clazz.is_a? Array
|
20
|
+
job_clazz, options = *job_clazz
|
21
|
+
batch_size = job_clazz._batch_size
|
22
|
+
job_clazz = job_clazz.set(**options)
|
23
|
+
else
|
24
|
+
batch_size = job_clazz._batch_size
|
25
|
+
end
|
26
|
+
|
27
|
+
next if job_clazz.nil?
|
28
|
+
|
29
|
+
if batch_size.nil?
|
30
|
+
job_clazz.perform_later(params_list)
|
31
|
+
else
|
32
|
+
params_list.each_slice(batch_size).each do |slice_params|
|
33
|
+
job_clazz.perform_later(slice_params)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TallyJobs
|
4
|
+
module TallyData
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.class_eval do
|
8
|
+
cattr_accessor :_batch_size
|
9
|
+
|
10
|
+
def self.batch_size(size)
|
11
|
+
self._batch_size = size
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def enqueue_to_tally(*params)
|
18
|
+
TallyJobs::JobsCounter.store.enqueue(self, *params)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/tally_jobs.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "tally_jobs/version"
|
4
|
+
require_relative "tally_jobs/engine"
|
5
|
+
require_relative "tally_jobs/configs"
|
6
|
+
require_relative "tally_jobs/jobs_counter"
|
7
|
+
require_relative "tally_jobs/tally_data"
|
8
|
+
require_relative "tally_jobs/configured_job"
|
9
|
+
|
10
|
+
module TallyJobs
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
mattr_accessor :runnable, default: true
|
14
|
+
mattr_reader :configs, default: TallyJobs::Configs.new
|
15
|
+
class << self
|
16
|
+
def setup
|
17
|
+
yield configs
|
18
|
+
|
19
|
+
TallyJobs.start
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module_function
|
24
|
+
|
25
|
+
# start a thread to flush in-memory enqueued jobs
|
26
|
+
# each {interval} time
|
27
|
+
MUTEX = Mutex.new
|
28
|
+
def start
|
29
|
+
MUTEX.synchronize do
|
30
|
+
@tally_thread = nil if !@tally_thread&.alive?
|
31
|
+
|
32
|
+
@tally_thread ||= Thread.new do
|
33
|
+
TallyJobs.configs.logger&.info("[TallyJobs] started ...")
|
34
|
+
while true
|
35
|
+
break unless TallyJobs.runnable
|
36
|
+
sleep(TallyJobs.configs.interval || 300)
|
37
|
+
|
38
|
+
JobsCounter.collect_then_perform_later
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def flush
|
45
|
+
TallyJobs.configs.logger&.info("[TallyJobs] flushing ...")
|
46
|
+
JobsCounter.collect_then_perform_later
|
47
|
+
end
|
48
|
+
|
49
|
+
def stop
|
50
|
+
flush
|
51
|
+
|
52
|
+
TallyJobs.runnable = false
|
53
|
+
if @tally_thread && @tally_thread.alive?
|
54
|
+
@tally_thread.wakeup
|
55
|
+
@tally_thread.join
|
56
|
+
end
|
57
|
+
TallyJobs.configs.logger&.info("[TallyJobs] stop ...")
|
58
|
+
end
|
59
|
+
|
60
|
+
def restart
|
61
|
+
stop
|
62
|
+
TallyJobs.runnable = true
|
63
|
+
start
|
64
|
+
end
|
65
|
+
|
66
|
+
# force flush all in-memory enqueued jobs before exit
|
67
|
+
at_exit do
|
68
|
+
TallyJobs.stop
|
69
|
+
end
|
70
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: tally_jobs
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- theforestvn88
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-04-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activejob
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: railties
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rails
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec-rails
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: redis
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: Collect all params of the same jobs within an interval time then enqueue
|
98
|
+
that job only one time.
|
99
|
+
email:
|
100
|
+
- theforestvn88@gmail.com
|
101
|
+
executables: []
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- README.md
|
106
|
+
- Rakefile
|
107
|
+
- lib/generators/tally_jobs/install_generator.rb
|
108
|
+
- lib/generators/templates/tally_jobs.rb
|
109
|
+
- lib/tally_jobs.rb
|
110
|
+
- lib/tally_jobs/configs.rb
|
111
|
+
- lib/tally_jobs/configured_job.rb
|
112
|
+
- lib/tally_jobs/counter_store.rb
|
113
|
+
- lib/tally_jobs/counter_store/memory_counter_store.rb
|
114
|
+
- lib/tally_jobs/counter_store/redis_counter_store.rb
|
115
|
+
- lib/tally_jobs/engine.rb
|
116
|
+
- lib/tally_jobs/jobs_counter.rb
|
117
|
+
- lib/tally_jobs/tally_data.rb
|
118
|
+
- lib/tally_jobs/version.rb
|
119
|
+
homepage: https://github.com/theforestvn88/tally_jobs.git
|
120
|
+
licenses: []
|
121
|
+
metadata:
|
122
|
+
homepage_uri: https://github.com/theforestvn88/tally_jobs.git
|
123
|
+
source_code_uri: https://github.com/theforestvn88/tally_jobs.git
|
124
|
+
changelog_uri: https://github.com/theforestvn88/tally_jobs.git
|
125
|
+
post_install_message:
|
126
|
+
rdoc_options: []
|
127
|
+
require_paths:
|
128
|
+
- lib
|
129
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
130
|
+
requirements:
|
131
|
+
- - ">="
|
132
|
+
- !ruby/object:Gem::Version
|
133
|
+
version: 1.0.0
|
134
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
requirements: []
|
140
|
+
rubygems_version: 3.4.10
|
141
|
+
signing_key:
|
142
|
+
specification_version: 4
|
143
|
+
summary: Collect all params of the same jobs within an interval time then enqueue
|
144
|
+
that job only one time.
|
145
|
+
test_files: []
|