threasy 0.0.1
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/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +84 -0
- data/Rakefile +5 -0
- data/lib/threasy/config.rb +16 -0
- data/lib/threasy/schedule.rb +104 -0
- data/lib/threasy/version.rb +3 -0
- data/lib/threasy/work.rb +97 -0
- data/lib/threasy.rb +34 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/threasy/schedule_spec.rb +45 -0
- data/spec/threasy/work_spec.rb +27 -0
- data/threasy.gemspec +27 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +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
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Carl Zulauf
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
# Threasy [![Build Status][travis-image]][travis-link]
|
2
|
+
|
3
|
+
[travis-image]: https://secure.travis-ci.org/carlzulauf/threasy.png?branch=master
|
4
|
+
[travis-link]: http://travis-ci.org/carlzulauf/threasy
|
5
|
+
|
6
|
+
Dead simple in-process threaded background job solution.
|
7
|
+
|
8
|
+
Includes scheduling for jobs in the future and/or recurring jobs.
|
9
|
+
|
10
|
+
Work waiting to be processed is stored in a thread-safe `Queue` and worked on by a small pool of worker threads.
|
11
|
+
|
12
|
+
### What to expect
|
13
|
+
|
14
|
+
* Dead simple-ness
|
15
|
+
* Ability to queue or schedule ruby blocks for asynchronus execution
|
16
|
+
* Extremely light-weight
|
17
|
+
* No dependencies (outside ruby stdlib)
|
18
|
+
* Portable: jruby, rubinius, and MRI
|
19
|
+
* Good performance
|
20
|
+
* Thread-safe
|
21
|
+
* Great solution for low to medium traffic single instance apps
|
22
|
+
* Ability to conserve memory on small VMs by doing as much in a single process as possible
|
23
|
+
* Plays nice with threaded or single process rack servers (puma, thin, rainbows)
|
24
|
+
|
25
|
+
### What __not__ to expect
|
26
|
+
|
27
|
+
* Failed job retrying/recovery (might be added, but just logs failures for now)
|
28
|
+
* Good solution for large scale deployments
|
29
|
+
* Avoidance of GIL contention issues in MRI during non-blocking jobs
|
30
|
+
* Plays nice with forking or other multi-process rack servers (passenger, unicorn)
|
31
|
+
|
32
|
+
## Installation
|
33
|
+
|
34
|
+
Add this line to your application's Gemfile:
|
35
|
+
|
36
|
+
gem 'threasy'
|
37
|
+
|
38
|
+
And then execute:
|
39
|
+
|
40
|
+
$ bundle
|
41
|
+
|
42
|
+
Or install it yourself as:
|
43
|
+
|
44
|
+
$ gem install threasy
|
45
|
+
|
46
|
+
## Usage
|
47
|
+
|
48
|
+
### `enqueue`
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Use a block
|
52
|
+
Threasy.enqueue{ puts "This will happen in the background" }
|
53
|
+
|
54
|
+
# Use an object that responds to #perform or #call
|
55
|
+
Threasy.enqueue MyJob.new(1,2,3)
|
56
|
+
```
|
57
|
+
|
58
|
+
### `schedule`
|
59
|
+
|
60
|
+
Puts a job onto the schedule. Once the scheduler sees a job is due for processessing, it is enqueued into the work queue to be processed like any other job.
|
61
|
+
|
62
|
+
Available options:
|
63
|
+
|
64
|
+
* `:every`: number of seconds between repetition. Can be combined with `:at` or `:in`.
|
65
|
+
* `:in`: number of seconds until job is (next) triggered.
|
66
|
+
* `:at`: `Time` job is (next) triggered.
|
67
|
+
|
68
|
+
Example:
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
# Use a block
|
72
|
+
Threasy.schedule(:in => 5.minutes) { puts "In the background, 5 minutes from now" }
|
73
|
+
|
74
|
+
# Use an job object
|
75
|
+
Threasy.schedule(MyJob.new(1,2,3), every: 5.minutes)
|
76
|
+
```
|
77
|
+
|
78
|
+
## Contributing
|
79
|
+
|
80
|
+
1. Fork it ( http://github.com/<my-github-username>/threasy/fork )
|
81
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
82
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
83
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
84
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Threasy
|
2
|
+
class Config
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_accessor :max_workers
|
6
|
+
attr_writer :logger
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
self.max_workers = 5
|
10
|
+
end
|
11
|
+
|
12
|
+
def logger
|
13
|
+
@logger ||= Logger.new(STDOUT).tap{|l| l.level = Logger::INFO }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module Threasy
|
2
|
+
class Schedule
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@semaphore = Mutex.new
|
7
|
+
@schedules = []
|
8
|
+
@watcher = Thread.new{ watch }
|
9
|
+
end
|
10
|
+
|
11
|
+
def add(*args, &block)
|
12
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
13
|
+
job = block_given? ? block : args.first
|
14
|
+
add_entry Entry.new(job, options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_entry(entry)
|
18
|
+
sync do
|
19
|
+
@schedules << entry
|
20
|
+
@schedules.sort_by!(&:at)
|
21
|
+
end
|
22
|
+
tickle_watcher
|
23
|
+
end
|
24
|
+
|
25
|
+
def tickle_watcher
|
26
|
+
@watcher.wakeup if @watcher.stop?
|
27
|
+
end
|
28
|
+
|
29
|
+
def sync
|
30
|
+
@semaphore.synchronize{ yield }
|
31
|
+
end
|
32
|
+
|
33
|
+
def entries
|
34
|
+
@schedules
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear
|
38
|
+
sync{ @schedules.clear }
|
39
|
+
end
|
40
|
+
|
41
|
+
def watch
|
42
|
+
loop do
|
43
|
+
Thread.stop if @schedules.empty?
|
44
|
+
entries_due.each do |entry|
|
45
|
+
log.debug "Adding scheduled job to work queue"
|
46
|
+
entry.work!
|
47
|
+
add_entry entry if entry.repeat?
|
48
|
+
end
|
49
|
+
next_job = @schedules.first
|
50
|
+
if next_job && next_job.future?
|
51
|
+
seconds = next_job.at - Time.now
|
52
|
+
log.debug "Schedule watcher sleeping for #{seconds} seconds"
|
53
|
+
sleep seconds
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def entries_due
|
59
|
+
[].tap do |entries|
|
60
|
+
sync do
|
61
|
+
while @schedules.first && @schedules.first.due?
|
62
|
+
entries << @schedules.shift
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def count
|
69
|
+
@schedules.count
|
70
|
+
end
|
71
|
+
|
72
|
+
def log
|
73
|
+
Threasy.logger
|
74
|
+
end
|
75
|
+
|
76
|
+
class Entry
|
77
|
+
attr_accessor :job, :at, :repeat
|
78
|
+
|
79
|
+
def initialize(job, options = {})
|
80
|
+
self.job = job
|
81
|
+
seconds = options[:in] || options[:every]
|
82
|
+
self.at = options.fetch(:at){ Time.now + seconds }
|
83
|
+
self.repeat = options[:every]
|
84
|
+
end
|
85
|
+
|
86
|
+
def repeat?
|
87
|
+
!! repeat
|
88
|
+
end
|
89
|
+
|
90
|
+
def due?
|
91
|
+
Time.now > at
|
92
|
+
end
|
93
|
+
|
94
|
+
def future?
|
95
|
+
! due?
|
96
|
+
end
|
97
|
+
|
98
|
+
def work!
|
99
|
+
Threasy.enqueue job
|
100
|
+
self.at = Time.now + repeat if repeat?
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/threasy/work.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module Threasy
|
2
|
+
class Work
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
attr_reader :queue, :pool
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@queue = TimeoutQueue.new
|
9
|
+
@pool = Set.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(job = nil, &block)
|
13
|
+
queue.push(block_given? ? block : job).tap{ check_workers }
|
14
|
+
end
|
15
|
+
|
16
|
+
alias_method :enqueue_block, :enqueue
|
17
|
+
|
18
|
+
def grab
|
19
|
+
queue.pop
|
20
|
+
end
|
21
|
+
|
22
|
+
def max_workers
|
23
|
+
Threasy.config.max_workers
|
24
|
+
end
|
25
|
+
|
26
|
+
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
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_worker
|
36
|
+
log "Adding new worker to pool"
|
37
|
+
Worker.new(pool.size).work(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
def log(msg)
|
41
|
+
Threasy.logger.debug msg
|
42
|
+
end
|
43
|
+
|
44
|
+
class Worker
|
45
|
+
def initialize(id)
|
46
|
+
@id = id
|
47
|
+
end
|
48
|
+
|
49
|
+
def work(work)
|
50
|
+
Thread.start do
|
51
|
+
work.pool.add Thread.current
|
52
|
+
while job = work.grab
|
53
|
+
log.debug "Worker ##{@id} has grabbed a job"
|
54
|
+
begin
|
55
|
+
job.respond_to?(:perform) ? job.perform : job.call
|
56
|
+
rescue Exception => e
|
57
|
+
log.error %|Worker ##{@id} error: #{e.message}\n#{e.backtrace.join("\n")}|
|
58
|
+
end
|
59
|
+
end
|
60
|
+
log.debug "Worker ##{@id} removing self from pool"
|
61
|
+
work.pool.delete Thread.current
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def log
|
66
|
+
Threasy.logger
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class TimeoutQueue
|
71
|
+
include Timeout
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
@queue = Queue.new
|
75
|
+
end
|
76
|
+
|
77
|
+
def push(item)
|
78
|
+
@queue << item
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def pop(seconds = 5)
|
83
|
+
timeout(seconds) { @queue.pop }
|
84
|
+
rescue Timeout::Error
|
85
|
+
nil
|
86
|
+
end
|
87
|
+
|
88
|
+
def size
|
89
|
+
@queue.size
|
90
|
+
end
|
91
|
+
|
92
|
+
def clear
|
93
|
+
@queue.clear
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
data/lib/threasy.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "singleton"
|
3
|
+
|
4
|
+
require "threasy/version"
|
5
|
+
require "threasy/config"
|
6
|
+
require "threasy/work"
|
7
|
+
require "threasy/schedule"
|
8
|
+
|
9
|
+
module Threasy
|
10
|
+
def self.config
|
11
|
+
yield Config.instance if block_given?
|
12
|
+
Config.instance
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.logger
|
16
|
+
config.logger
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.work
|
20
|
+
Work.instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.enqueue(*args, &block)
|
24
|
+
work.enqueue *args, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.schedules
|
28
|
+
Schedule.instance
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.schedule(*args, &block)
|
32
|
+
schedules.add *args, &block
|
33
|
+
end
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), "..", "lib", "threasy")
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Threasy::Schedule" do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@schedule = Threasy::Schedule.instance
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#add" do
|
10
|
+
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
|
15
|
+
end
|
16
|
+
|
17
|
+
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
|
22
|
+
end
|
23
|
+
|
24
|
+
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
|
29
|
+
end
|
30
|
+
|
31
|
+
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)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
after :each do
|
42
|
+
@schedule.clear
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe "Threasy::Work" do
|
4
|
+
before :each do
|
5
|
+
@work = Threasy::Work.instance
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#enqueue" do
|
9
|
+
it "should allow a job object to be enqueued and worked" do
|
10
|
+
job = double("job")
|
11
|
+
expect(job).to receive(:perform).once
|
12
|
+
@work.enqueue job
|
13
|
+
sleep 0.5
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should allow a job block to be enqueued and worked" do
|
17
|
+
i = 0
|
18
|
+
@work.enqueue do
|
19
|
+
sleep 0.5
|
20
|
+
i += 1
|
21
|
+
end
|
22
|
+
expect(i).to eq(0)
|
23
|
+
sleep 1
|
24
|
+
expect(i).to eq(1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/threasy.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'threasy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "threasy"
|
8
|
+
s.version = Threasy::VERSION
|
9
|
+
s.authors = ["Carl Zulauf"]
|
10
|
+
s.email = ["carl@linkleaf.com"]
|
11
|
+
s.summary = %q{Simple threaded background jobs and scheduling.}
|
12
|
+
s.description = %q{Dead simple in-process background job solution using threads, with support for scheduled jobs.}
|
13
|
+
s.homepage = "http://github.com/carlzulauf/threasy"
|
14
|
+
s.license = "MIT"
|
15
|
+
|
16
|
+
s.files = %w( Gemfile README.md Rakefile LICENSE.txt threasy.gemspec )
|
17
|
+
s.files += Dir.glob("lib/**/*")
|
18
|
+
s.files += Dir.glob("spec/**/*")
|
19
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
|
23
|
+
s.add_development_dependency "bundler", "~> 1.5"
|
24
|
+
s.add_development_dependency "rake"
|
25
|
+
s.add_development_dependency "pry"
|
26
|
+
s.add_development_dependency "rspec"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: threasy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Carl Zulauf
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
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"
|
20
|
+
name: bundler
|
21
|
+
type: :development
|
22
|
+
requirement: *id001
|
23
|
+
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"
|
31
|
+
name: rake
|
32
|
+
type: :development
|
33
|
+
requirement: *id002
|
34
|
+
prerelease: false
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
version_requirements: &id004 !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- *id003
|
39
|
+
name: pry
|
40
|
+
type: :development
|
41
|
+
requirement: *id004
|
42
|
+
prerelease: false
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
version_requirements: &id005 !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- *id003
|
47
|
+
name: rspec
|
48
|
+
type: :development
|
49
|
+
requirement: *id005
|
50
|
+
prerelease: false
|
51
|
+
description: Dead simple in-process background job solution using threads, with support for scheduled jobs.
|
52
|
+
email:
|
53
|
+
- carl@linkleaf.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files: []
|
59
|
+
|
60
|
+
files:
|
61
|
+
- Gemfile
|
62
|
+
- LICENSE.txt
|
63
|
+
- README.md
|
64
|
+
- Rakefile
|
65
|
+
- lib/threasy.rb
|
66
|
+
- lib/threasy/config.rb
|
67
|
+
- lib/threasy/schedule.rb
|
68
|
+
- lib/threasy/version.rb
|
69
|
+
- lib/threasy/work.rb
|
70
|
+
- spec/spec_helper.rb
|
71
|
+
- spec/threasy/schedule_spec.rb
|
72
|
+
- spec/threasy/work_spec.rb
|
73
|
+
- threasy.gemspec
|
74
|
+
homepage: http://github.com/carlzulauf/threasy
|
75
|
+
licenses:
|
76
|
+
- MIT
|
77
|
+
metadata: {}
|
78
|
+
|
79
|
+
post_install_message:
|
80
|
+
rdoc_options: []
|
81
|
+
|
82
|
+
require_paths:
|
83
|
+
- 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
|
90
|
+
requirements: []
|
91
|
+
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.2.1
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Simple threaded background jobs and scheduling.
|
97
|
+
test_files:
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
- spec/threasy/schedule_spec.rb
|
100
|
+
- spec/threasy/work_spec.rb
|