threasy 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: