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