steady 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.
- 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
|