threasy 0.0.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
- ---
2
- SHA1:
3
- metadata.gz: b06dfdee1ca788c21cf8cdd036eb0b7b7995bba3
4
- data.tar.gz: ae7b941674e8d3db405fd8227ce206a3f0e7f62e
5
- SHA512:
6
- metadata.gz: d7ce1d2e0829dfca7ae76891df339164fdf379a647d3e601c416dccd7eabb91412cf8bc647bbc97fb4e5c5613a46a83917e15f55aa0ec7a2fc56786b6c38f3ac
7
- data.tar.gz: 7ef14d1a0917a5104a0508697ee7d486f07601eef86e6057860336705da471039debc1ddf4b80697566eb5c7d7ec10b8a71316b26cfd4614b6b4ecdea9b5e4e9
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 36bcb6a7e87480c94e15a521e2d74a7de07ed995
4
+ data.tar.gz: 7f049f5eb6155d16b14278e599544268cf3426cc
5
+ SHA512:
6
+ metadata.gz: 21fc8a8219c66db4fc212dd6a992dcea86b68f22bb5b3102c5e2c628636dadb23a17b138d759dcc982a6abe46581fd151953250e19425094b7093c83bce915cc
7
+ data.tar.gz: c38a34ed3220b5ed5c2b804ff714f9fabf8d413959d647bef757f8585cd5bfbd884746deddce9aec1e056003ccd03e5d8871a056f29c9ac3bb8496ca32e11809
data/Gemfile CHANGED
@@ -2,7 +2,6 @@ source 'https://rubygems.org'
2
2
 
3
3
  platform :rbx do
4
4
  gem "rubysl-mutex_m"
5
- gem "rubysl-singleton"
6
5
  end
7
6
 
8
7
  gemspec
data/lib/threasy.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  require "logger"
2
- require "singleton"
3
2
 
4
3
  require "threasy/version"
5
4
  require "threasy/config"
@@ -8,8 +7,9 @@ require "threasy/schedule"
8
7
 
9
8
  module Threasy
10
9
  def self.config
11
- yield Config.instance if block_given?
12
- Config.instance
10
+ @@config ||= Config.new
11
+ yield @@config if block_given?
12
+ @@config
13
13
  end
14
14
 
15
15
  def self.logger
@@ -17,7 +17,7 @@ module Threasy
17
17
  end
18
18
 
19
19
  def self.work
20
- Work.instance
20
+ config.work ||= Work.new
21
21
  end
22
22
 
23
23
  def self.enqueue(*args, &block)
@@ -25,7 +25,7 @@ module Threasy
25
25
  end
26
26
 
27
27
  def self.schedules
28
- Schedule.instance
28
+ config.schedule ||= Schedule.new(work)
29
29
  end
30
30
 
31
31
  def self.schedule(*args, &block)
@@ -1,8 +1,6 @@
1
1
  module Threasy
2
2
  class Config
3
- include Singleton
4
-
5
- attr_accessor :max_workers
3
+ attr_accessor :work, :schedule, :max_workers
6
4
  attr_writer :logger
7
5
 
8
6
  def initialize
@@ -1,8 +1,11 @@
1
1
  module Threasy
2
2
  class Schedule
3
- include Singleton
3
+ MAX_SLEEP = 5.0 # 5 seconds
4
4
 
5
- def initialize
5
+ include Enumerable
6
+
7
+ def initialize(work = nil)
8
+ @work = work
6
9
  @semaphore = Mutex.new
7
10
  @schedules = []
8
11
  @watcher = Thread.new{ watch }
@@ -11,7 +14,7 @@ module Threasy
11
14
  def add(*args, &block)
12
15
  options = args.last.is_a?(Hash) ? args.pop : {}
13
16
  job = block_given? ? block : args.first
14
- add_entry Entry.new(job, options)
17
+ add_entry Entry.new(self, job, options)
15
18
  end
16
19
 
17
20
  def add_entry(entry)
@@ -20,6 +23,15 @@ module Threasy
20
23
  @schedules.sort_by!(&:at)
21
24
  end
22
25
  tickle_watcher
26
+ entry
27
+ end
28
+
29
+ def work
30
+ @work ||= Threasy.work
31
+ end
32
+
33
+ def remove_entry(entry)
34
+ sync{ @schedules.delete entry }
23
35
  end
24
36
 
25
37
  def tickle_watcher
@@ -27,14 +39,19 @@ module Threasy
27
39
  end
28
40
 
29
41
  def sync
30
- @semaphore.synchronize{ yield }
42
+ @semaphore.synchronize { yield }
31
43
  end
32
44
 
33
45
  def entries
34
46
  @schedules
35
47
  end
36
48
 
49
+ def each
50
+ entries.each { |entry| yield entry }
51
+ end
52
+
37
53
  def clear
54
+ log.debug "Clearing schedules"
38
55
  sync{ @schedules.clear }
39
56
  end
40
57
 
@@ -48,7 +65,7 @@ module Threasy
48
65
  end
49
66
  next_job = @schedules.first
50
67
  if next_job && next_job.future?
51
- seconds = next_job.at - Time.now
68
+ seconds = [next_job.at - Time.now, MAX_SLEEP].min
52
69
  log.debug "Schedule watcher sleeping for #{seconds} seconds"
53
70
  sleep seconds
54
71
  end
@@ -74,19 +91,26 @@ module Threasy
74
91
  end
75
92
 
76
93
  class Entry
77
- attr_accessor :job, :at, :repeat
94
+ MAX_OVERDUE = 300 # 5 minutes
78
95
 
79
- def initialize(job, options = {})
96
+ attr_accessor :schedule, :job, :at, :repeat
97
+
98
+ def initialize(schedule, job, options = {})
99
+ self.schedule = schedule
80
100
  self.job = job
81
- seconds = options[:in] || options[:every]
82
- self.at = options.fetch(:at){ Time.now + seconds }
83
101
  self.repeat = options[:every]
102
+ seconds = options.fetch(:in){ repeat || 60 }
103
+ self.at = options.fetch(:at){ Time.now + seconds }
84
104
  end
85
105
 
86
106
  def repeat?
87
107
  !! repeat
88
108
  end
89
109
 
110
+ def once?
111
+ ! repeat?
112
+ end
113
+
90
114
  def due?
91
115
  Time.now > at
92
116
  end
@@ -95,9 +119,19 @@ module Threasy
95
119
  ! due?
96
120
  end
97
121
 
122
+ def overdue
123
+ Time.now - at
124
+ end
125
+
98
126
  def work!
99
- Threasy.enqueue job
100
- self.at = Time.now + repeat if repeat?
127
+ if once? || overdue < MAX_OVERDUE
128
+ schedule.work.enqueue job
129
+ end
130
+ self.at = at + repeat if repeat?
131
+ end
132
+
133
+ def remove
134
+ schedule.remove_entry self
101
135
  end
102
136
  end
103
137
  end
@@ -1,3 +1,3 @@
1
1
  module Threasy
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
data/lib/threasy/work.rb CHANGED
@@ -1,12 +1,11 @@
1
1
  module Threasy
2
2
  class Work
3
- include Singleton
4
-
5
3
  attr_reader :queue, :pool
6
4
 
7
5
  def initialize
8
6
  @queue = TimeoutQueue.new
9
7
  @pool = Set.new
8
+ @semaphore = Mutex.new
10
9
  end
11
10
 
12
11
  def enqueue(job = nil, &block)
@@ -15,6 +14,10 @@ module Threasy
15
14
 
16
15
  alias_method :enqueue_block, :enqueue
17
16
 
17
+ def sync(&block)
18
+ @semaphore.synchronize &block
19
+ end
20
+
18
21
  def grab
19
22
  queue.pop
20
23
  end
@@ -24,17 +27,23 @@ module Threasy
24
27
  end
25
28
 
26
29
  def check_workers
27
- pool_size = pool.size
28
- queue_size = queue.size
29
- log "Checking workers. Pool: #{pool_size}, Max: #{max_workers}, Queue: #{queue_size}"
30
- if pool_size < max_workers
31
- add_worker if pool_size == 0 || queue_size > max_workers
30
+ sync do
31
+ pool_size = pool.size
32
+ queue_size = queue.size
33
+ log "Checking workers. Pool: #{pool_size}, Max: #{max_workers}, Queue: #{queue_size}"
34
+ if pool_size < max_workers
35
+ add_worker(pool_size) if pool_size == 0 || queue_size > max_workers
36
+ end
32
37
  end
33
38
  end
34
39
 
35
- def add_worker
36
- log "Adding new worker to pool"
37
- Worker.new(pool.size).work(self)
40
+ def add_worker(size)
41
+ # sync do
42
+ log "Adding new worker to pool"
43
+ worker = Worker.new(self, size)
44
+ pool.add worker
45
+ # end
46
+ worker.work
38
47
  end
39
48
 
40
49
  def log(msg)
@@ -42,23 +51,24 @@ module Threasy
42
51
  end
43
52
 
44
53
  class Worker
45
- def initialize(id)
54
+ def initialize(work, id)
55
+ @work = work
46
56
  @id = id
47
57
  end
48
58
 
49
- def work(work)
59
+ def work
50
60
  Thread.start do
51
- work.pool.add Thread.current
52
- while job = work.grab
61
+ while job = @work.grab
53
62
  log.debug "Worker ##{@id} has grabbed a job"
54
63
  begin
64
+ job = eval(job) if job.kind_of?(String)
55
65
  job.respond_to?(:perform) ? job.perform : job.call
56
66
  rescue Exception => e
57
67
  log.error %|Worker ##{@id} error: #{e.message}\n#{e.backtrace.join("\n")}|
58
68
  end
59
69
  end
60
70
  log.debug "Worker ##{@id} removing self from pool"
61
- work.pool.delete Thread.current
71
+ @work.sync{ @work.pool.delete self }
62
72
  end
63
73
  end
64
74
 
data/spec/spec_helper.rb CHANGED
@@ -1 +1,6 @@
1
+ # require 'pry'
2
+ require 'timecop'
3
+
1
4
  require File.join(File.dirname(__FILE__), "..", "lib", "threasy")
5
+
6
+ # Threasy.config.logger.level = Logger::DEBUG
@@ -1,45 +1,102 @@
1
- require "spec_helper"
2
-
3
1
  describe "Threasy::Schedule" do
4
-
5
- before :each do
6
- @schedule = Threasy::Schedule.instance
7
- end
2
+ let(:job){ double("job") }
3
+ let(:work){ double("work") }
4
+ subject{ Threasy::Schedule.new(work) }
8
5
 
9
6
  describe "#add" do
10
7
  it "should allow a job to be processed after specified delay" do
11
- job = double("job")
12
- expect(job).to receive(:perform)
13
- @schedule.add(job, in: 0.5)
14
- sleep 1
8
+ expect(work).to receive(:enqueue).with(job)
9
+ subject.add(job, in: 0.1)
10
+ sleep 0.2
15
11
  end
16
12
 
17
13
  it "should allow a job to be processed at the specified time" do
18
- job = double("job")
19
- expect(job).to receive(:perform)
20
- @schedule.add(job, at: Time.now + 1)
21
- sleep 2
14
+ expect(work).to receive(:enqueue).with(job)
15
+ subject.add(job, at: Time.now + 0.1)
16
+ sleep 0.2
22
17
  end
23
18
 
24
19
  it "should allow a job to be repeated at the specified interval" do
25
- job = double("job")
26
- expect(job).to receive(:perform).at_least(:twice)
27
- @schedule.add(job, every: 1)
28
- sleep 3
20
+ expect(work).to receive(:enqueue).with(job).at_least(:twice)
21
+ subject.add(job, every: 0.1, in: 0.1)
22
+ sleep 0.3
29
23
  end
30
24
 
31
25
  it "should allow blocks to be processed on schedule" do
32
- job = double("job")
33
- i = 0
34
- @schedule.add(:in => 0.5){ i += 1 }
35
- expect(i).to eq(0)
36
- sleep 1
37
- expect(i).to eq(1)
26
+ block_job = ->{ 1 + 1 }
27
+ expect(work).to receive(:enqueue).with(block_job)
28
+ subject.add(in: 0.1, &block_job)
29
+ sleep 0.2
30
+ end
31
+
32
+ it "should allow string expressions to be processed on schedule" do
33
+ expect(work).to receive(:enqueue).with("TestScheduledJob")
34
+ subject.add("TestScheduledJob", in: 0.1)
35
+ sleep 0.2
36
+ end
37
+
38
+ it "should default first run to now + every_interval" do
39
+ expect(work).to receive(:enqueue).with(job).at_least(:twice)
40
+ subject.add(job, every: 0.1)
41
+ sleep 0.3
42
+ end
43
+ end
44
+
45
+ describe "#remove" do
46
+ it "should be possible to remove a job from the schedule" do
47
+ expect(work).to receive(:enqueue).with(job).at_least(:once).at_most(:twice)
48
+ entry = subject.add(job, every: 0.2)
49
+ sleep 0.3
50
+ entry.remove
51
+ sleep 0.5
38
52
  end
39
53
  end
40
54
 
41
- after :each do
42
- @schedule.clear
55
+ context "when laptop suspends" do
56
+ subject{ Threasy::Schedule.new }
57
+ let(:hour) { 60*60 }
58
+
59
+ it "should recover in a few seconds when time suddenly jumps forward" do
60
+ job_ran = false
61
+ job = -> { job_ran = true }
62
+ subject.add(in: hour + 1, &job)
63
+
64
+ Timecop.travel(Time.now + hour) do
65
+ Timeout.timeout(6) do
66
+ loop { job_ran ? break : sleep(0.2) }
67
+ end
68
+ end
69
+ end
70
+
71
+ it "should execute a one-time schedule long after it's due" do
72
+ job_ran = false
73
+ job = -> { job_ran = true }
74
+ subject.add(in: hour/2, &job)
75
+
76
+ Timecop.travel(Time.now + hour) do
77
+ Timeout.timeout(6) do
78
+ loop { job_ran ? break : sleep(0.2) }
79
+ end
80
+ end
81
+ end
82
+
83
+ # This really tests Schedule::Entry and should be moved to its own spec
84
+ it "should skip a repeating schedule that is long past due" do
85
+ job1_ran = false
86
+ job1 = -> { job1_ran = true }
87
+
88
+ job2_ran = false
89
+ job2 = -> { job2_ran = true }
90
+
91
+ subject.add(every: hour, in: hour/2, &job1)
92
+ subject.add(in: hour/2, &job2)
93
+
94
+ Timecop.travel(Time.now + hour) { sleep 6 }
95
+
96
+ expect(job1_ran).to eq(false)
97
+ expect(job2_ran).to eq(true)
98
+ end
99
+
43
100
  end
44
101
 
45
102
  end
@@ -1,27 +1,38 @@
1
- require "spec_helper"
1
+ class TestJob
2
+ end
2
3
 
3
4
  describe "Threasy::Work" do
4
5
  before :each do
5
- @work = Threasy::Work.instance
6
+ @work = Threasy::Work.new
6
7
  end
7
8
 
8
9
  describe "#enqueue" do
10
+ it "should have a method for enqueing" do
11
+ expect(@work.respond_to?(:enqueue)).to eq(true)
12
+ end
13
+
9
14
  it "should allow a job object to be enqueued and worked" do
10
15
  job = double("job")
11
16
  expect(job).to receive(:perform).once
12
17
  @work.enqueue job
13
- sleep 0.5
18
+ sleep 0.1
14
19
  end
15
20
 
16
21
  it "should allow a job block to be enqueued and worked" do
17
22
  i = 0
18
23
  @work.enqueue do
19
- sleep 0.5
24
+ sleep 0.1
20
25
  i += 1
21
26
  end
22
27
  expect(i).to eq(0)
23
- sleep 1
28
+ sleep 0.2
24
29
  expect(i).to eq(1)
25
30
  end
31
+
32
+ it "should allow a string expression to be enqueued and worked" do
33
+ expect(TestJob).to receive(:perform).once
34
+ @work.enqueue "TestJob"
35
+ sleep 0.1
36
+ end
26
37
  end
27
38
  end
data/threasy.gemspec CHANGED
@@ -24,4 +24,5 @@ Gem::Specification.new do |s|
24
24
  s.add_development_dependency "rake"
25
25
  s.add_development_dependency "pry"
26
26
  s.add_development_dependency "rspec"
27
+ s.add_development_dependency "timecop"
27
28
  end
metadata CHANGED
@@ -1,63 +1,93 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: threasy
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.1
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
5
  platform: ruby
6
- authors:
6
+ authors:
7
7
  - Carl Zulauf
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
-
12
- date: 2014-01-21 00:00:00 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- version_requirements: &id001 !ruby/object:Gem::Requirement
16
- requirements:
17
- - - ~>
18
- - !ruby/object:Gem::Version
19
- version: "1.5"
11
+ date: 2015-08-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
20
14
  name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
21
20
  type: :development
22
- requirement: *id001
23
21
  prerelease: false
24
- - !ruby/object:Gem::Dependency
25
- version_requirements: &id002 !ruby/object:Gem::Requirement
26
- requirements:
27
- - &id003
28
- - ">="
29
- - !ruby/object:Gem::Version
30
- version: "0"
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
31
28
  name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
32
34
  type: :development
33
- requirement: *id002
34
35
  prerelease: false
35
- - !ruby/object:Gem::Dependency
36
- version_requirements: &id004 !ruby/object:Gem::Requirement
37
- requirements:
38
- - *id003
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
39
42
  name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
40
48
  type: :development
41
- requirement: *id004
42
49
  prerelease: false
43
- - !ruby/object:Gem::Dependency
44
- version_requirements: &id005 !ruby/object:Gem::Requirement
45
- requirements:
46
- - *id003
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
47
56
  name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
48
62
  type: :development
49
- requirement: *id005
50
63
  prerelease: false
51
- description: Dead simple in-process background job solution using threads, with support for scheduled jobs.
52
- email:
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: timecop
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
+ description: Dead simple in-process background job solution using threads, with support
84
+ for scheduled jobs.
85
+ email:
53
86
  - carl@linkleaf.com
54
87
  executables: []
55
-
56
88
  extensions: []
57
-
58
89
  extra_rdoc_files: []
59
-
60
- files:
90
+ files:
61
91
  - Gemfile
62
92
  - LICENSE.txt
63
93
  - README.md
@@ -72,29 +102,31 @@ files:
72
102
  - spec/threasy/work_spec.rb
73
103
  - threasy.gemspec
74
104
  homepage: http://github.com/carlzulauf/threasy
75
- licenses:
105
+ licenses:
76
106
  - MIT
77
107
  metadata: {}
78
-
79
108
  post_install_message:
80
109
  rdoc_options: []
81
-
82
- require_paths:
110
+ require_paths:
83
111
  - lib
84
- required_ruby_version: !ruby/object:Gem::Requirement
85
- requirements:
86
- - *id003
87
- required_rubygems_version: !ruby/object:Gem::Requirement
88
- requirements:
89
- - *id003
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ required_rubygems_version: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
90
122
  requirements: []
91
-
92
123
  rubyforge_project:
93
- rubygems_version: 2.2.1
124
+ rubygems_version: 2.4.5
94
125
  signing_key:
95
126
  specification_version: 4
96
127
  summary: Simple threaded background jobs and scheduling.
97
- test_files:
128
+ test_files:
98
129
  - spec/spec_helper.rb
99
130
  - spec/threasy/schedule_spec.rb
100
131
  - spec/threasy/work_spec.rb
132
+ has_rdoc: