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.
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'thread'
2
+ require 'thread_job/job'
3
+ require 'thread_job/job_store'
4
+ require 'thread_job/backends/memory/store'
5
+ require 'thread_job/scheduler'
6
+ require 'thread_job/thread_pool'
@@ -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,7 @@
1
+ module ThreadJob
2
+ class Job
3
+ def run
4
+ raise Exception.new 'not implemented'
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ module ThreadJob
2
+ class JobStore
3
+ def save_job(queue_name, job_name, job)
4
+ end
5
+
6
+ def poll_for_job(queue_name)
7
+ end
8
+
9
+ def complete_job(queue_name, job_name)
10
+ end
11
+
12
+ def fail_job(queue_name, job_name)
13
+ end
14
+ end
15
+ 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
@@ -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: []