edamame 0.2.0
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/LICENSE.textile +20 -0
- data/README.textile +90 -0
- data/app/edamame_san/config.ru +4 -0
- data/app/edamame_san/config.yml +17 -0
- data/app/edamame_san/edamame_san.rb +71 -0
- data/app/edamame_san/public/favicon.ico +0 -0
- data/app/edamame_san/public/images/edamame_logo.icns +0 -0
- data/app/edamame_san/public/images/edamame_logo.ico +0 -0
- data/app/edamame_san/public/images/edamame_logo.png +0 -0
- data/app/edamame_san/public/images/edamame_logo_2.icns +0 -0
- data/app/edamame_san/public/javascripts/application.js +8 -0
- data/app/edamame_san/public/javascripts/jquery/jquery-ui.js +8694 -0
- data/app/edamame_san/public/javascripts/jquery/jquery.js +4376 -0
- data/app/edamame_san/public/stylesheets/application.css +32 -0
- data/app/edamame_san/public/stylesheets/layout.css +88 -0
- data/app/edamame_san/views/layout.haml +13 -0
- data/app/edamame_san/views/load.haml +37 -0
- data/app/edamame_san/views/root.haml +25 -0
- data/bin/edamame-nuke +20 -0
- data/bin/edamame-ps +2 -0
- data/bin/edamame-stats +13 -0
- data/bin/edamame-sync +21 -0
- data/bin/edamame_util_opts.rb +10 -0
- data/bin/test_run.rb +14 -0
- data/lib/edamame.rb +29 -0
- data/lib/edamame/broker.rb +38 -0
- data/lib/edamame/job.rb +114 -0
- data/lib/edamame/monitoring.rb +7 -0
- data/lib/edamame/monitoring/README-god.textile +54 -0
- data/lib/edamame/monitoring/beanstalkd_god.rb +28 -0
- data/lib/edamame/monitoring/god_email.rb +45 -0
- data/lib/edamame/monitoring/god_process.rb +205 -0
- data/lib/edamame/monitoring/process_groups.rb +32 -0
- data/lib/edamame/monitoring/sinatra_god.rb +34 -0
- data/lib/edamame/monitoring/tyrant_god.rb +59 -0
- data/lib/edamame/persistent_queue.rb +152 -0
- data/lib/edamame/queue.rb +6 -0
- data/lib/edamame/queue/beanstalk.rb +134 -0
- data/lib/edamame/scheduling.rb +79 -0
- data/lib/edamame/store.rb +8 -0
- data/lib/edamame/store/base.rb +62 -0
- data/lib/edamame/store/tyrant_store.rb +49 -0
- data/lib/methods.txt +94 -0
- data/spec/edamame_spec.rb +7 -0
- data/spec/spec_helper.rb +10 -0
- data/utils/god/edamame.god +36 -0
- data/utils/god/edamame.yaml +61 -0
- data/utils/god/god-etc-init-dot-d-example +40 -0
- data/utils/god/god.conf +22 -0
- data/utils/god/god_site_config.rb +4 -0
- data/utils/god/wuclan.god +36 -0
- data/utils/simulation/Add Percent Variation.vi +0 -0
- data/utils/simulation/Harmonic Average.vi +0 -0
- data/utils/simulation/Rescheduling Simulation.aliases +3 -0
- data/utils/simulation/Rescheduling Simulation.lvlps +3 -0
- data/utils/simulation/Rescheduling Simulation.lvproj +22 -0
- data/utils/simulation/Rescheduling.vi +0 -0
- data/utils/simulation/Weighted Average.vi +0 -0
- metadata +147 -0
@@ -0,0 +1,152 @@
|
|
1
|
+
module Edamame
|
2
|
+
class PersistentQueue
|
3
|
+
DEFAULT_OPTIONS = {
|
4
|
+
:queue => { :type => :beanstalk_queue, :uris => ['localhost:11100'] },
|
5
|
+
:store => { :type => :tyrant_store, :uri => ':11101' }
|
6
|
+
}
|
7
|
+
# Hash of options used to create this Queue. Don't mess with this after
|
8
|
+
# creating the object -- it will be futile, at best.
|
9
|
+
attr_reader :options
|
10
|
+
# The default tube for the transient queue
|
11
|
+
# Tube name must be purely alphanumeric
|
12
|
+
attr_reader :tube
|
13
|
+
# The database backing store to use, probably a Edamame::Store
|
14
|
+
attr_reader :store
|
15
|
+
# The priority queue to use, probably a Edamame::Queue
|
16
|
+
attr_reader :queue
|
17
|
+
#
|
18
|
+
# Create a PersistentQueue with options
|
19
|
+
#
|
20
|
+
# @param [Hash] options the options to create a message with.
|
21
|
+
# @option options [String] :tube The default tube for the transient queue
|
22
|
+
# @option options [String] :queue Option hash for the Edamame::Queue
|
23
|
+
# @option options [String] :store Option hash for the Edamame::Store
|
24
|
+
#
|
25
|
+
def initialize _options={}
|
26
|
+
@options = PersistentQueue::DEFAULT_OPTIONS.deep_merge(_options)
|
27
|
+
@tube = options[:tube] || :default
|
28
|
+
@store = Edamame::Store.create options[:store]
|
29
|
+
@queue = Edamame::Queue.create options[:queue].merge(:default_tube => @tube)
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Add a new Job to the queue
|
34
|
+
#
|
35
|
+
def put job, *args
|
36
|
+
job.tube = self.tube if job.tube.blank?
|
37
|
+
self.tube = job.tube
|
38
|
+
return if store.include?(job.key)
|
39
|
+
store.save job
|
40
|
+
queue.put job, *args
|
41
|
+
end
|
42
|
+
# Alias for put(job)
|
43
|
+
def << job
|
44
|
+
put job
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the default tube
|
48
|
+
def tube= _tube
|
49
|
+
return if @tube == _tube
|
50
|
+
puts "#{self.class} setting tube to #{_tube}, was #{@tube}"
|
51
|
+
queue.tube = @tube = _tube
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retrieve named record
|
55
|
+
def get key, klass=nil
|
56
|
+
klass ||= Edamame::Job
|
57
|
+
hsh = store.get(key) or return
|
58
|
+
klass.from_hash hsh
|
59
|
+
end
|
60
|
+
|
61
|
+
#
|
62
|
+
# Request a job fom the queue for processing
|
63
|
+
#
|
64
|
+
def reserve timeout=nil, klass=nil
|
65
|
+
qjob = queue.reserve(timeout) or return
|
66
|
+
job = get(qjob.key, klass) or return
|
67
|
+
job.qjob = qjob
|
68
|
+
job
|
69
|
+
end
|
70
|
+
|
71
|
+
#
|
72
|
+
# Remove the job from the queue.
|
73
|
+
#
|
74
|
+
def delete job
|
75
|
+
store.delete job.key
|
76
|
+
queue.delete job.qjob
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Returns the job to the queue, to be re-run later.
|
81
|
+
#
|
82
|
+
# release'ing a job acknowledges it was completed, successfully or not
|
83
|
+
#
|
84
|
+
def release job
|
85
|
+
job.update!
|
86
|
+
store.save job
|
87
|
+
queue.release job.qjob, job.priority, job.scheduling.delay
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Returns each job as it appears in the queue.
|
92
|
+
#
|
93
|
+
# all jobs -- active, inactive, running, etc -- are returned,
|
94
|
+
# and in some arbitrary order.
|
95
|
+
#
|
96
|
+
def each klass=nil, &block
|
97
|
+
klass ||= Edamame::Job
|
98
|
+
store.each_as(klass) do |key, job|
|
99
|
+
yield job
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Loads all jobs from the backing store into the queue.
|
105
|
+
#
|
106
|
+
def load &block
|
107
|
+
hoard do |job|
|
108
|
+
yield(job) if block
|
109
|
+
unless store.include?(job.key)
|
110
|
+
warn "Missing job: #{job.inspect}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
unhoard &block
|
114
|
+
end
|
115
|
+
|
116
|
+
# Returns a hash of stats about the store and queue
|
117
|
+
def stats
|
118
|
+
{ :store_stats => store.stats,
|
119
|
+
:queue_stats => queue.stats,
|
120
|
+
:tube => self.tube }
|
121
|
+
end
|
122
|
+
|
123
|
+
protected
|
124
|
+
#
|
125
|
+
# Destructively strips the beanstalkd queue of all of its jobs.
|
126
|
+
#
|
127
|
+
# This is the only way (I know) to enumerate all of the jobs in the queue --
|
128
|
+
# certainly the only way that respects concurrency.
|
129
|
+
#
|
130
|
+
# You shouldn't use this in general; the point of the backing store is to
|
131
|
+
# allow exactly such queries and enumeration. See #each instead.
|
132
|
+
#
|
133
|
+
def hoard &block
|
134
|
+
queue.empty tube, &block
|
135
|
+
end
|
136
|
+
|
137
|
+
#
|
138
|
+
# Loads all jobs from the backing store into the queue.
|
139
|
+
#
|
140
|
+
# The queue must be emptied of all jobs before running this command:
|
141
|
+
# otherwise jobs will be duplicated.
|
142
|
+
#
|
143
|
+
def unhoard klass=nil, &block
|
144
|
+
each(klass) do |job|
|
145
|
+
self.tube = job.tube
|
146
|
+
yield(job) if block
|
147
|
+
queue.put job, job.priority, Edamame::IMMEDIATELY
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Edamame
|
2
|
+
module Queue
|
3
|
+
#
|
4
|
+
# Persistent job queue for periodic requests.
|
5
|
+
#
|
6
|
+
# Jobs are reserved, run, and if successful put back with an updated delay parameter.
|
7
|
+
#
|
8
|
+
# This is useful for mass scraping of timelines (RSS feeds, twitter search
|
9
|
+
# results, etc. See http://github.com/mrflip/wuclan for )
|
10
|
+
#
|
11
|
+
class BeanstalkQueue
|
12
|
+
DEFAULT_OPTIONS = {
|
13
|
+
:priority => 65536, # default job queue priority
|
14
|
+
:time_to_run => 60*5, # 5 minutes to complete a job or assume dead
|
15
|
+
:uris => ['localhost:11300'],
|
16
|
+
:default_tube => 'default',
|
17
|
+
}
|
18
|
+
attr_accessor :options
|
19
|
+
|
20
|
+
#
|
21
|
+
# beanstalk_pool -- specify nil to use the default single-node ['localhost:11300'] pool
|
22
|
+
#
|
23
|
+
def initialize _options={}
|
24
|
+
self.options = DEFAULT_OPTIONS.deep_merge(_options.compact)
|
25
|
+
options[:default_tube] = options[:default_tube].to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
#
|
29
|
+
# Add a new Qjob to the queue
|
30
|
+
#
|
31
|
+
def put job, priority=nil, delay=nil
|
32
|
+
beanstalk.put(job.key, (priority || job.priority), (delay || job.delay), job.ttr)
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# Remove the qjob from the queue.
|
37
|
+
#
|
38
|
+
def delete(qjob)
|
39
|
+
qjob.delete
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Returns the qjob to the queue, to be re-run later.
|
44
|
+
#
|
45
|
+
# release'ing a qjob acknowledges it was completed, successfully or not
|
46
|
+
#
|
47
|
+
def release qjob, priority=nil, delay=nil
|
48
|
+
qjob.release( (priority || qjob.priority), (delay || qjob.delay) )
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Take the next (highest priority, delay met) qjob.
|
53
|
+
# Set timeout (default is 10s)
|
54
|
+
# Returns nil on error or timeout. Interrupt error passes through
|
55
|
+
#
|
56
|
+
def reserve timeout=10
|
57
|
+
begin
|
58
|
+
qjob = beanstalk.reserve(timeout) or return
|
59
|
+
rescue Beanstalk::TimedOut => e ; warn e.to_s ; sleep 0.4 ; return ;
|
60
|
+
rescue StandardError => e ; warn e.to_s ; sleep 1 ; return ; end
|
61
|
+
qjob
|
62
|
+
end
|
63
|
+
|
64
|
+
#
|
65
|
+
# Shelves the qjob.
|
66
|
+
#
|
67
|
+
def bury
|
68
|
+
qjob.bury qjob.priority
|
69
|
+
end
|
70
|
+
|
71
|
+
# The beanstalk pool which acts as job queue
|
72
|
+
def beanstalk
|
73
|
+
return @beanstalk if @beanstalk
|
74
|
+
@beanstalk = Beanstalk::Pool.new(options[:uris], options[:default_tube])
|
75
|
+
self.tube= options[:default_tube]
|
76
|
+
@beanstalk
|
77
|
+
end
|
78
|
+
# Close the job queue
|
79
|
+
def close
|
80
|
+
@beanstalk.close if @beanstalk
|
81
|
+
@beanstalk = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# uses and watches the given beanstalk tube
|
85
|
+
def tube= _tube
|
86
|
+
puts "#{self.class} setting tube to #{_tube}, was #{@tube}"
|
87
|
+
@beanstalk.use _tube
|
88
|
+
@beanstalk.watch _tube
|
89
|
+
end
|
90
|
+
|
91
|
+
# Stats on job count across the pool
|
92
|
+
def stats
|
93
|
+
beanstalk.stats.select{|k,v| k =~ /jobs/}
|
94
|
+
end
|
95
|
+
# Total jobs in the queue, whether reserved, ready, buried or delayed.
|
96
|
+
def current_jobs
|
97
|
+
beanstalk.
|
98
|
+
stats.
|
99
|
+
select{|k,v| (k =~ /jobs/) && (k != 'total-jobs')}.
|
100
|
+
inject(0){|sum,kv| sum += kv.last }
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
#
|
105
|
+
#
|
106
|
+
def empty tube=nil, &block
|
107
|
+
tube = tube.to_s if tube
|
108
|
+
curr_tube = beanstalk.list_tube_used.values.first
|
109
|
+
curr_watches = beanstalk.list_tubes_watched.values.first
|
110
|
+
beanstalk.use tube if tube
|
111
|
+
beanstalk.watch tube if tube
|
112
|
+
p ["emptying", tube, current_jobs]
|
113
|
+
loop do
|
114
|
+
kicked = beanstalk.open_connections.map{|conxn| conxn.kick(20) }
|
115
|
+
break if (current_jobs == 0) || (!beanstalk.peek_ready)
|
116
|
+
qjob = reserve(5) or break
|
117
|
+
yield qjob
|
118
|
+
qjob.delete
|
119
|
+
end
|
120
|
+
beanstalk.use curr_tube
|
121
|
+
beanstalk.ignore tube if (! curr_watches.include?(tube))
|
122
|
+
end
|
123
|
+
|
124
|
+
def empty_all &block
|
125
|
+
tubes = beanstalk.list_tubes.values.flatten.uniq
|
126
|
+
tubes.each do |tube|
|
127
|
+
empty tube, &block
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end # class
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'wukong/extensions/hashlike_class'
|
2
|
+
module Edamame
|
3
|
+
# sugar for rescheduled jobs
|
4
|
+
IMMEDIATELY = 0
|
5
|
+
|
6
|
+
module Scheduling
|
7
|
+
extend FactoryModule
|
8
|
+
|
9
|
+
# def type
|
10
|
+
# self.class.to_s
|
11
|
+
# end
|
12
|
+
# def to_hash
|
13
|
+
# end
|
14
|
+
|
15
|
+
class Base
|
16
|
+
include Wukong::HashlikeClass
|
17
|
+
has_members :last_run, :total_runs
|
18
|
+
|
19
|
+
def initialize *args
|
20
|
+
members.zip(args).each do |key, val|
|
21
|
+
self[key] = val if val
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def last_run_time
|
26
|
+
last_run.is_a?(String) ? Time.parse(last_run) : last_run
|
27
|
+
end
|
28
|
+
|
29
|
+
def since_last
|
30
|
+
Time.now - last_run_time
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class Every < Base
|
36
|
+
has_member :delay
|
37
|
+
end
|
38
|
+
|
39
|
+
class At < Base
|
40
|
+
attr_accessor :time
|
41
|
+
def initialize *args
|
42
|
+
super *args
|
43
|
+
self.time = Time.parse(time) if time.is_a?(String)
|
44
|
+
end
|
45
|
+
def delay
|
46
|
+
@delay ||= time - Time.now
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Once < Base
|
51
|
+
def delay
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
#
|
57
|
+
# A recurring task
|
58
|
+
#
|
59
|
+
# * Run every once in a while -- often enough that you don't miss anything
|
60
|
+
#
|
61
|
+
# want to scrape everything between now and prev_item
|
62
|
+
#
|
63
|
+
# * at the previous run, objects up to prev_time and prev_id
|
64
|
+
# * in the current run, objects up to curr_time and curr_id
|
65
|
+
# * average rate
|
66
|
+
#
|
67
|
+
class Recurring < Base
|
68
|
+
has_members :delay, :prev_max, :prev_items, :prev_items_rate
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# :total_items, :goal_items,
|
73
|
+
# cattr_accessor :min_resched_delay, :max_resched_delay
|
74
|
+
# self.min_resched_delay = 10
|
75
|
+
# self.max_resched_delay = 24*60*60
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# require 'monkeyshines/utils/factory_module'
|
2
|
+
module Edamame
|
3
|
+
module Store
|
4
|
+
class Base
|
5
|
+
# The actual backing store; should respond to #set and #get methods
|
6
|
+
attr_accessor :db
|
7
|
+
|
8
|
+
def initialize options
|
9
|
+
end
|
10
|
+
|
11
|
+
#
|
12
|
+
# Executes block once for each element in the whole DB, in whatever order
|
13
|
+
# the DB thinks you should see it.
|
14
|
+
#
|
15
|
+
# Your block will see |key, val|
|
16
|
+
#
|
17
|
+
# key_store.each do |key, val|
|
18
|
+
# # ... stuff ...
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
def each &block
|
22
|
+
db.iterinit
|
23
|
+
loop do
|
24
|
+
key = db.iternext or break
|
25
|
+
val = db[key]
|
26
|
+
yield key, val
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def each_as klass, &block
|
31
|
+
self.each do |key, hsh|
|
32
|
+
yield [key, klass.from_hash(hsh)]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Delegate to store
|
37
|
+
def set(key, val)
|
38
|
+
return unless val
|
39
|
+
db.put key, val.to_hash.compact
|
40
|
+
end
|
41
|
+
def save obj
|
42
|
+
return unless obj
|
43
|
+
db.put obj.key, obj.to_hash.compact
|
44
|
+
end
|
45
|
+
|
46
|
+
def get(key) db[key] end
|
47
|
+
def [](key) get(key) end
|
48
|
+
def put(key, val) db.put key, val end
|
49
|
+
def close() db.close end
|
50
|
+
def size() db.size end
|
51
|
+
def delete(key) db.delete(key) end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Load from standard command-line options
|
55
|
+
#
|
56
|
+
# obvs only works when there's just one store
|
57
|
+
#
|
58
|
+
def self.create type, options
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'tokyotyrant'
|
2
|
+
module Edamame
|
3
|
+
module Store
|
4
|
+
|
5
|
+
#
|
6
|
+
# Implementation of KeyStore with a Local TokyoCabinet table database (TDB)
|
7
|
+
#
|
8
|
+
class TyrantStore < Edamame::Store::Base
|
9
|
+
attr_accessor :db_host, :db_port
|
10
|
+
|
11
|
+
# pass in the host:port uri of the key store.
|
12
|
+
def initialize options
|
13
|
+
self.db_host, self.db_port = options[:uri].to_s.split(':')
|
14
|
+
super options
|
15
|
+
end
|
16
|
+
|
17
|
+
def db
|
18
|
+
return @db if @db
|
19
|
+
@db ||= TokyoTyrant::RDBTBL.new
|
20
|
+
@db.open(db_host, db_port) or raise("Can't open DB #{db_host}:#{db_port}. Pass in host:port' #{@db.ecode}: #{@db.errmsg(@db.ecode)}")
|
21
|
+
@db
|
22
|
+
end
|
23
|
+
|
24
|
+
def close
|
25
|
+
@db.close if @db
|
26
|
+
@db = nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Save the value into the database without waiting for a response.
|
30
|
+
def set_nr(key, val)
|
31
|
+
db.putnr key, val if val
|
32
|
+
end
|
33
|
+
|
34
|
+
def size()
|
35
|
+
db.rnum
|
36
|
+
end
|
37
|
+
|
38
|
+
def stats
|
39
|
+
{ :size => size }
|
40
|
+
end
|
41
|
+
|
42
|
+
def include? *args
|
43
|
+
db.has_key? *args
|
44
|
+
end
|
45
|
+
|
46
|
+
end # TyrantStore
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|