thread_job 0.1.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 +7 -0
- data/lib/thread_job.rb +6 -0
- data/lib/thread_job/backends/memory/store.rb +105 -0
- data/lib/thread_job/job.rb +7 -0
- data/lib/thread_job/job_store.rb +15 -0
- data/lib/thread_job/scheduler.rb +59 -0
- data/lib/thread_job/thread_pool.rb +83 -0
- data/thread_job.gemspec +17 -0
- metadata +66 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA1:
|
|
3
|
+
metadata.gz: 7bd76842b19742485270bbe794a33f9f852f0133
|
|
4
|
+
data.tar.gz: 4a5926023d47505c75b56155090d8ddb38ea97c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 441dd1924c0b8b2b008e3127c9329d5b029bf9b38b29e94f06a90e68765e838a27616c077210d7500dd9ac4af16ee98a0b33eff59ded44d4324d245e13faf806
|
|
7
|
+
data.tar.gz: 7b1136aaa31fc69684e807b8db0e4e521d07276f27ef405b1abbf466299a752c6e7be2ce5f8a96b6a9ce902981d7d7b19ede76fd42c27d08e4b0994d7dac88ac
|
data/lib/thread_job.rb
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module ThreadJob
|
|
4
|
+
module Memory
|
|
5
|
+
AVAILABLE = 1
|
|
6
|
+
WORKING = 2
|
|
7
|
+
COMPLETE = 3
|
|
8
|
+
FAILED = 4
|
|
9
|
+
|
|
10
|
+
class Record
|
|
11
|
+
attr_accessor :id, :attempts, :job, :job_name, :queue_name, :status
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Store < JobStore
|
|
15
|
+
def initialize(max_retries=10, logger=Logger.new(STDOUT))
|
|
16
|
+
@jobs = {}
|
|
17
|
+
@failed_jobs = {}
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
@logger = logger
|
|
20
|
+
@max_retries = max_retries
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def save_job(queue_name, job_name, job)
|
|
24
|
+
@mutex.synchronize {
|
|
25
|
+
queued_jobs = @jobs[queue_name] ||= []
|
|
26
|
+
failed_queue_jobs = @failed_jobs[queue_name] ||= []
|
|
27
|
+
|
|
28
|
+
rec = Memory::Record.new
|
|
29
|
+
rec.attempts = 0
|
|
30
|
+
rec.id = queued_jobs.count + 1
|
|
31
|
+
rec.job_name = job_name
|
|
32
|
+
rec.job = job
|
|
33
|
+
rec.status = AVAILABLE
|
|
34
|
+
rec.queue_name = queue_name
|
|
35
|
+
|
|
36
|
+
queued_jobs.push(rec)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@logger.info("[MemoryStore] Saved #{job_name}")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def poll_for_job(queue_name)
|
|
43
|
+
@jobs[queue_name] ||= []
|
|
44
|
+
@logger.debug("[MemoryStore] Polling for jobs, #{@jobs[queue_name].length} in the queue")
|
|
45
|
+
|
|
46
|
+
@mutex.synchronize {
|
|
47
|
+
@jobs[queue_name].each do |record|
|
|
48
|
+
if record.status == AVAILABLE || (record.status == FAILED && record.attempts < @max_retries)
|
|
49
|
+
record.status = WORKING
|
|
50
|
+
@logger.debug("[MemoryStore] Sending job '#{record.job_name}' to the thread pool for work")
|
|
51
|
+
return {id: record.id, job: record.job, job_name: record.job_name}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def get_job(queue_name, job_id)
|
|
60
|
+
found_job = false
|
|
61
|
+
if @jobs[queue_name] != nil
|
|
62
|
+
@jobs[queue_name].each do |job|
|
|
63
|
+
if job.id == job_id
|
|
64
|
+
found_job = true
|
|
65
|
+
return job
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@logger.warn("[MemoryStore] unable to get job: #{job_id} from queue: #{queue_name}")
|
|
71
|
+
|
|
72
|
+
return nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def complete_job(queue_name, job_id)
|
|
76
|
+
@mutex.synchronize {
|
|
77
|
+
job = get_job(queue_name, job_id)
|
|
78
|
+
if job
|
|
79
|
+
@jobs[queue_name].delete(job)
|
|
80
|
+
@logger.info("[MemoryStore] job: '#{job.job_name}' has been completed and removed from the queue")
|
|
81
|
+
end
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fail_job(queue_name, job_id)
|
|
86
|
+
@mutex.synchronize {
|
|
87
|
+
job = get_job(queue_name, job_id)
|
|
88
|
+
if job
|
|
89
|
+
job.status = FAILED
|
|
90
|
+
job.attempts += 1
|
|
91
|
+
|
|
92
|
+
if job.attempts == @max_retries
|
|
93
|
+
@failed_jobs[queue_name].push(job)
|
|
94
|
+
@jobs[queue_name].delete(job)
|
|
95
|
+
@logger.warn("[MemoryStore] job: '#{job.job_name}' has failed the reached the maximum amount of retries (#{@max_retries}) and is being removed from the queue.")
|
|
96
|
+
else
|
|
97
|
+
@logger.info("[MemoryStore] failed job: '#{job.job_name}' has been requeued and attempted #{job.attempts} times")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module ThreadJob
|
|
4
|
+
class Scheduler
|
|
5
|
+
|
|
6
|
+
def initialize(queue_name, job_store=ThreadJob::Memory::Store.new, poll_delay_seconds=5, thread_pool_size=5, logger=Logger.new(STDOUT))
|
|
7
|
+
@job_store = job_store
|
|
8
|
+
@logger = logger
|
|
9
|
+
@queue_name = queue_name
|
|
10
|
+
@poll_delay = poll_delay_seconds
|
|
11
|
+
@scheduler_thread = nil
|
|
12
|
+
@thread_pool = ThreadPool.new(thread_pool_size, logger)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def start
|
|
16
|
+
return @scheduler_thread = Thread.new do
|
|
17
|
+
do_start
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def kill
|
|
22
|
+
@logger.info("[Scheduler] scheduler stopping...")
|
|
23
|
+
@scheduler_thread.kill
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def kill_workers
|
|
27
|
+
@logger.info("[Scheduler] Stopping all worker threads")
|
|
28
|
+
@thread_pool.kill
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def add_job(job_name, job)
|
|
32
|
+
@logger.info("[Scheduler] Added job: '#{job_name}' to the '#{@queue_name}' queue")
|
|
33
|
+
@job_store.save_job(@queue_name, job_name, job)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def add_workers(num_workers)
|
|
37
|
+
@logger.info("[Scheduler] Adding #{num_workers} to the worker pool")
|
|
38
|
+
@thread_pool.add_workers(num_workers)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
def do_start
|
|
43
|
+
@logger.info("[Scheduler] starting...")
|
|
44
|
+
while true
|
|
45
|
+
if @thread_pool.has_available_thread?
|
|
46
|
+
job_hash = @job_store.poll_for_job(@queue_name)
|
|
47
|
+
if job_hash
|
|
48
|
+
job_hash[:queue_name] = @queue_name
|
|
49
|
+
job_hash[:job_store] = @job_store
|
|
50
|
+
@logger.info("[Scheduler] scheudled job '#{job_hash[:job_name]}', sending to thread pool")
|
|
51
|
+
@thread_pool.run(job_hash)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
sleep(@poll_delay)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'logger'
|
|
2
|
+
|
|
3
|
+
module ThreadJob
|
|
4
|
+
class ThreadPool
|
|
5
|
+
|
|
6
|
+
def initialize(max_size=5, logger=Logger.new(STDOUT))
|
|
7
|
+
@queue = Queue.new
|
|
8
|
+
@logger = logger
|
|
9
|
+
@avail_pool = max_size.times.map do
|
|
10
|
+
Thread.new do
|
|
11
|
+
@logger.debug("[ThreadPool] started thread #{Thread.current}")
|
|
12
|
+
while true
|
|
13
|
+
monitor_queue
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
@use_pool = []
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def has_available_thread?
|
|
22
|
+
@mutex.synchronize {
|
|
23
|
+
@logger.debug("[ThreadPool] #{@avail_pool.length} threads available, #{@use_pool.length} threads in use")
|
|
24
|
+
return @avail_pool.length > 0
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add_workers(num_workers)
|
|
29
|
+
num_workers.times do
|
|
30
|
+
thread = Thread.new do
|
|
31
|
+
@logger.debug("[ThreadPool] started thread #{Thread.current}")
|
|
32
|
+
while true
|
|
33
|
+
monitor_queue
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
@avail_pool.push(thread)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def kill
|
|
41
|
+
@avail_pool.each do |avail_thread|
|
|
42
|
+
avail_thread.kill
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@use_pool.each do |used_thread|
|
|
46
|
+
used_thread.kill
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def monitor_queue
|
|
51
|
+
work = @queue.pop
|
|
52
|
+
if work
|
|
53
|
+
@mutex.synchronize {
|
|
54
|
+
@use_pool.push(Thread.current)
|
|
55
|
+
@avail_pool.delete(Thread.current)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@logger.debug("[ThreadPool] Running job '#{work[:job_name]}' on thread #{Thread.current}")
|
|
59
|
+
begin
|
|
60
|
+
work[:job].run
|
|
61
|
+
rescue => e
|
|
62
|
+
@logger.error("[ThreadPool] Worker thread #{Thread.current} encountered an error #{e} while processing job '#{work[:job_name]}'")
|
|
63
|
+
work[:job_store].fail_job(work[:queue_name], work[:id])
|
|
64
|
+
@mutex.synchronize {
|
|
65
|
+
@avail_pool.push(Thread.current)
|
|
66
|
+
@use_pool.delete(Thread.current)
|
|
67
|
+
}
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
work[:job_store].complete_job(work[:queue_name], work[:id])
|
|
71
|
+
|
|
72
|
+
@mutex.synchronize {
|
|
73
|
+
@avail_pool.push(Thread.current)
|
|
74
|
+
@use_pool.delete(Thread.current)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def run(job_hash)
|
|
80
|
+
@queue.push(job_hash)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
data/thread_job.gemspec
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.authors = ['Jeff Kuchta']
|
|
5
|
+
spec.description = 'A simple framework to asynchronously execute longer running tasks in the background using threads.'
|
|
6
|
+
spec.email = ['jeff@flywiremedia.com']
|
|
7
|
+
spec.files = %w[thread_job.gemspec]
|
|
8
|
+
spec.files += Dir.glob('{lib}/**/*')
|
|
9
|
+
spec.homepage = 'https://github.com/jkuchta/thread_job'
|
|
10
|
+
spec.licenses = ['MIT']
|
|
11
|
+
spec.name = 'thread_job'
|
|
12
|
+
spec.require_paths = ['lib']
|
|
13
|
+
spec.summary = 'Lightweight asynchronous scheduler and workers'
|
|
14
|
+
#spec.test_files = Dir.glob('spec/**/*')
|
|
15
|
+
spec.add_development_dependency 'rspec'
|
|
16
|
+
spec.version = '0.1.0'
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: thread_job
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jeff Kuchta
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2017-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
description: A simple framework to asynchronously execute longer running tasks in
|
|
28
|
+
the background using threads.
|
|
29
|
+
email:
|
|
30
|
+
- jeff@flywiremedia.com
|
|
31
|
+
executables: []
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- lib/thread_job.rb
|
|
36
|
+
- lib/thread_job/backends/memory/store.rb
|
|
37
|
+
- lib/thread_job/job.rb
|
|
38
|
+
- lib/thread_job/job_store.rb
|
|
39
|
+
- lib/thread_job/scheduler.rb
|
|
40
|
+
- lib/thread_job/thread_pool.rb
|
|
41
|
+
- thread_job.gemspec
|
|
42
|
+
homepage: https://github.com/jkuchta/thread_job
|
|
43
|
+
licenses:
|
|
44
|
+
- MIT
|
|
45
|
+
metadata: {}
|
|
46
|
+
post_install_message:
|
|
47
|
+
rdoc_options: []
|
|
48
|
+
require_paths:
|
|
49
|
+
- lib
|
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
requirements: []
|
|
61
|
+
rubyforge_project:
|
|
62
|
+
rubygems_version: 2.5.1
|
|
63
|
+
signing_key:
|
|
64
|
+
specification_version: 4
|
|
65
|
+
summary: Lightweight asynchronous scheduler and workers
|
|
66
|
+
test_files: []
|