threasy 0.0.1

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