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 +7 -7
- data/Gemfile +0 -1
- data/lib/threasy.rb +5 -5
- data/lib/threasy/config.rb +1 -3
- data/lib/threasy/schedule.rb +45 -11
- data/lib/threasy/version.rb +1 -1
- data/lib/threasy/work.rb +25 -15
- data/spec/spec_helper.rb +5 -0
- data/spec/threasy/schedule_spec.rb +83 -26
- data/spec/threasy/work_spec.rb +16 -5
- data/threasy.gemspec +1 -0
- metadata +83 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
---
|
2
|
-
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
5
|
-
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
-
|
12
|
-
|
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.
|
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.
|
28
|
+
config.schedule ||= Schedule.new(work)
|
29
29
|
end
|
30
30
|
|
31
31
|
def self.schedule(*args, &block)
|
data/lib/threasy/config.rb
CHANGED
data/lib/threasy/schedule.rb
CHANGED
@@ -1,8 +1,11 @@
|
|
1
1
|
module Threasy
|
2
2
|
class Schedule
|
3
|
-
|
3
|
+
MAX_SLEEP = 5.0 # 5 seconds
|
4
4
|
|
5
|
-
|
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
|
-
|
94
|
+
MAX_OVERDUE = 300 # 5 minutes
|
78
95
|
|
79
|
-
|
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
|
-
|
100
|
-
|
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
|
data/lib/threasy/version.rb
CHANGED
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
-
|
37
|
-
|
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
|
59
|
+
def work
|
50
60
|
Thread.start do
|
51
|
-
work.
|
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
|
71
|
+
@work.sync{ @work.pool.delete self }
|
62
72
|
end
|
63
73
|
end
|
64
74
|
|
data/spec/spec_helper.rb
CHANGED
@@ -1,45 +1,102 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
|
3
1
|
describe "Threasy::Schedule" do
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
42
|
-
|
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
|
data/spec/threasy/work_spec.rb
CHANGED
@@ -1,27 +1,38 @@
|
|
1
|
-
|
1
|
+
class TestJob
|
2
|
+
end
|
2
3
|
|
3
4
|
describe "Threasy::Work" do
|
4
5
|
before :each do
|
5
|
-
@work = Threasy::Work.
|
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.
|
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.
|
24
|
+
sleep 0.1
|
20
25
|
i += 1
|
21
26
|
end
|
22
27
|
expect(i).to eq(0)
|
23
|
-
sleep
|
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
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
|
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
|
-
|
13
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
52
|
-
|
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
|
-
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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.
|
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:
|