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 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
@@ -0,0 +1,8 @@
1
+ source 'https://rubygems.org'
2
+
3
+ platform :rbx do
4
+ gem "rubysl-mutex_m"
5
+ gem "rubysl-singleton"
6
+ end
7
+
8
+ gemspec
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,5 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+ task :default => [:spec]
@@ -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
@@ -0,0 +1,3 @@
1
+ module Threasy
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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