steady 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +10 -0
- data/README.md +30 -0
- data/Rakefile +14 -0
- data/lib/steady.rb +134 -0
- data/lib/steady/version.rb +3 -0
- data/steady.gemspec +20 -0
- data/test/steady_test.rb +81 -0
- data/test/task_test.rb +50 -0
- metadata +73 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
pkg
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# Steady
|
2
|
+
|
3
|
+
This gem is aimed at helping speed up ruby webapps by putting reoccuring but non time critical tasks into a background thread.
|
4
|
+
It aids with the periodic scheduling as well as the threading issues that arise from moving data between threads.
|
5
|
+
|
6
|
+
# Example
|
7
|
+
|
8
|
+
Scheduler = Steady::Scheduler.new
|
9
|
+
|
10
|
+
Scheduler.every 3.seconds do |changes|
|
11
|
+
changes[:plans] = JSON.parse(open("http://mysite.com/plans.json"))
|
12
|
+
end
|
13
|
+
|
14
|
+
# Run all above tasks now to get initial data
|
15
|
+
Scheduler.run
|
16
|
+
|
17
|
+
# Schedule a thread to do this periodically
|
18
|
+
Scheduler.schedule
|
19
|
+
|
20
|
+
|
21
|
+
# Access your data in a thread safe manner
|
22
|
+
Scheduler.data[:plans]
|
23
|
+
|
24
|
+
# Uses at Shopify
|
25
|
+
|
26
|
+
* Monitoring read slave lagginess
|
27
|
+
* Fetching blog posts to show in the admin
|
28
|
+
* Fetching centrally configured beta flags from remote
|
29
|
+
* and many many more
|
30
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'rake'
|
5
|
+
require 'rake/testtask'
|
6
|
+
require 'bundler'
|
7
|
+
require 'bundler/gem_tasks'
|
8
|
+
|
9
|
+
Rake::TestTask.new do |t|
|
10
|
+
t.libs << '.' << 'lib' << 'test'
|
11
|
+
t.test_files = FileList['test/**/*_test.rb']
|
12
|
+
t.verbose = false
|
13
|
+
end
|
14
|
+
|
data/lib/steady.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'speedytime'
|
2
|
+
require 'thread'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module Steady
|
6
|
+
|
7
|
+
class SyncronizedHash
|
8
|
+
def initialize
|
9
|
+
@hash = Hash.new
|
10
|
+
@mutex = Mutex.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def [](key)
|
14
|
+
@mutex.synchronize { @hash[key] }
|
15
|
+
end
|
16
|
+
|
17
|
+
def []=(key)
|
18
|
+
@mutex.synchronize { @hash.[]=(key, value) }
|
19
|
+
end
|
20
|
+
|
21
|
+
def apply(changes)
|
22
|
+
@mutex.synchronize { @hash.merge!(changes) }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class DirtyTrackingHash < Hash
|
27
|
+
def dirty?
|
28
|
+
@dirty
|
29
|
+
end
|
30
|
+
|
31
|
+
def []=(key, value)
|
32
|
+
@dirty = true
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Scheduler
|
38
|
+
attr_reader :tasks, :data
|
39
|
+
|
40
|
+
def initialize
|
41
|
+
@tasks = []
|
42
|
+
@data = SyncronizedHash.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def every(interval, &block)
|
46
|
+
push Task.new(interval, &block)
|
47
|
+
end
|
48
|
+
|
49
|
+
def run
|
50
|
+
changes = DirtyTrackingHash.new
|
51
|
+
runs = false
|
52
|
+
|
53
|
+
|
54
|
+
@tasks.each do |task|
|
55
|
+
if task.needs_running?
|
56
|
+
task.run(changes)
|
57
|
+
runs = true
|
58
|
+
else
|
59
|
+
# tasks are sorted, if a tasks doesn't need running
|
60
|
+
# then no task behind it will need running either at
|
61
|
+
# the moment.
|
62
|
+
break
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# if tasks ran we resort the tasks and apply
|
67
|
+
# any changes to the data hash
|
68
|
+
if runs
|
69
|
+
@tasks.sort!
|
70
|
+
@data.apply(changes) if changes.dirty?
|
71
|
+
true
|
72
|
+
else
|
73
|
+
false
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def schedule
|
78
|
+
Thread.new do
|
79
|
+
loop { sleep; run }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def drowsiness
|
84
|
+
drowsiness = (t = @tasks.first) ? t.next_run - Speedytime.current : 1
|
85
|
+
drowsiness = 1 if drowsiness < 1
|
86
|
+
drowsiness
|
87
|
+
end
|
88
|
+
|
89
|
+
def sleep
|
90
|
+
sleep(drowsiness)
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def push(task)
|
96
|
+
@tasks.push(task)
|
97
|
+
@tasks.sort!
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
class Task
|
103
|
+
attr_accessor :last_run, :interval
|
104
|
+
|
105
|
+
def initialize(interval, &block)
|
106
|
+
@interval = interval
|
107
|
+
@proc = block
|
108
|
+
@last_run = 0
|
109
|
+
end
|
110
|
+
|
111
|
+
def next_run
|
112
|
+
@last_run + @interval
|
113
|
+
end
|
114
|
+
|
115
|
+
def needs_running?
|
116
|
+
next_run <= Speedytime.current
|
117
|
+
end
|
118
|
+
|
119
|
+
def run(changes = nil)
|
120
|
+
if needs_running?
|
121
|
+
@proc.call(changes)
|
122
|
+
@last_run = Speedytime.current
|
123
|
+
true
|
124
|
+
else
|
125
|
+
false
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def <=>(other)
|
130
|
+
next_run <=> other.next_run
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
data/steady.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'steady/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "steady"
|
8
|
+
gem.version = Steady::VERSION
|
9
|
+
gem.authors = ["Tobias Lutke"]
|
10
|
+
gem.email = ["tobi@shopify.com"]
|
11
|
+
gem.description = %q{Periodically run tasks that fetch data}
|
12
|
+
gem.summary = %q{Simple worker thread for steady tasks}
|
13
|
+
gem.homepage = ""
|
14
|
+
gem.files = `git ls-files`.split($/)
|
15
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
16
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
|
19
|
+
gem.add_dependency('speedytime')
|
20
|
+
end
|
data/test/steady_test.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "steady"
|
3
|
+
|
4
|
+
class SteadyTask < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
@sched = Steady::Scheduler.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_tasks_run
|
11
|
+
i = 0
|
12
|
+
@sched.every 1 do
|
13
|
+
i += 1
|
14
|
+
end
|
15
|
+
|
16
|
+
assert_equal 1, @sched.tasks.length
|
17
|
+
@sched.run
|
18
|
+
assert_equal 1, @sched.tasks.length
|
19
|
+
|
20
|
+
assert_equal 1, i
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_task_sorting
|
24
|
+
i = 0
|
25
|
+
|
26
|
+
@sched.every 2 do
|
27
|
+
i += 1
|
28
|
+
end
|
29
|
+
@sched.every 5 do
|
30
|
+
i += 1
|
31
|
+
end
|
32
|
+
@sched.every 1 do
|
33
|
+
i += 1
|
34
|
+
end
|
35
|
+
|
36
|
+
@sched.run
|
37
|
+
assert_equal 3, i
|
38
|
+
|
39
|
+
sleep 1.1
|
40
|
+
|
41
|
+
@sched.run
|
42
|
+
assert_equal 4, i
|
43
|
+
|
44
|
+
sleep 1.1
|
45
|
+
|
46
|
+
@sched.run
|
47
|
+
assert_equal 6, i
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_task_drowsiness
|
51
|
+
|
52
|
+
@sched.every 5 do
|
53
|
+
end
|
54
|
+
|
55
|
+
assert_equal 1, @sched.drowsiness
|
56
|
+
|
57
|
+
@sched.run
|
58
|
+
|
59
|
+
assert_equal 5, @sched.drowsiness
|
60
|
+
|
61
|
+
@sched.every 1 do
|
62
|
+
end
|
63
|
+
|
64
|
+
assert_equal 1, @sched.drowsiness
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_mege
|
69
|
+
|
70
|
+
assert_equal nil, @sched.data[:test]
|
71
|
+
|
72
|
+
@sched.every 1 do |changes|
|
73
|
+
changes[:test] = 'ok'
|
74
|
+
end
|
75
|
+
|
76
|
+
@sched.run
|
77
|
+
|
78
|
+
assert_equal 'ok', @sched.data[:test]
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
data/test/task_test.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
require "test/unit"
|
2
|
+
require "periodic"
|
3
|
+
|
4
|
+
class TestTask < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def test_tasks_run_by_default
|
7
|
+
i = 0
|
8
|
+
|
9
|
+
task = Periodic::Task.new(5) do
|
10
|
+
i += 1
|
11
|
+
end
|
12
|
+
|
13
|
+
task.run
|
14
|
+
|
15
|
+
assert_equal 1, i
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_tasks_run_one_per_second
|
19
|
+
i = 0
|
20
|
+
|
21
|
+
task = Periodic::Task.new(1) do
|
22
|
+
i += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
task.run
|
26
|
+
task.run
|
27
|
+
sleep 1.1
|
28
|
+
task.run
|
29
|
+
task.run
|
30
|
+
|
31
|
+
assert_equal 2, i, 'should have been 2 because only 2 unique seconds elapsed'
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_needs_running?
|
35
|
+
task = Periodic::Task.new(1)
|
36
|
+
assert task.needs_running?
|
37
|
+
task.last_run = Speedytime.current
|
38
|
+
assert !task.needs_running?
|
39
|
+
task.last_run = Speedytime.current - 5
|
40
|
+
assert task.needs_running?
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_interval
|
44
|
+
task = Periodic::Task.new(1)
|
45
|
+
task.last_run = Speedytime.current - 5
|
46
|
+
assert task.needs_running?
|
47
|
+
task.interval = 6
|
48
|
+
assert !task.needs_running?
|
49
|
+
end
|
50
|
+
end
|
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: steady
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tobias Lutke
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: speedytime
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: Periodically run tasks that fetch data
|
31
|
+
email:
|
32
|
+
- tobi@shopify.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- Gemfile.lock
|
40
|
+
- README.md
|
41
|
+
- Rakefile
|
42
|
+
- lib/steady.rb
|
43
|
+
- lib/steady/version.rb
|
44
|
+
- steady.gemspec
|
45
|
+
- test/steady_test.rb
|
46
|
+
- test/task_test.rb
|
47
|
+
homepage: ''
|
48
|
+
licenses: []
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
none: false
|
55
|
+
requirements:
|
56
|
+
- - ! '>='
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ! '>='
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
requirements: []
|
66
|
+
rubyforge_project:
|
67
|
+
rubygems_version: 1.8.23
|
68
|
+
signing_key:
|
69
|
+
specification_version: 3
|
70
|
+
summary: Simple worker thread for steady tasks
|
71
|
+
test_files:
|
72
|
+
- test/steady_test.rb
|
73
|
+
- test/task_test.rb
|