edamame 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. data/LICENSE.textile +20 -0
  2. data/README.textile +90 -0
  3. data/app/edamame_san/config.ru +4 -0
  4. data/app/edamame_san/config.yml +17 -0
  5. data/app/edamame_san/edamame_san.rb +71 -0
  6. data/app/edamame_san/public/favicon.ico +0 -0
  7. data/app/edamame_san/public/images/edamame_logo.icns +0 -0
  8. data/app/edamame_san/public/images/edamame_logo.ico +0 -0
  9. data/app/edamame_san/public/images/edamame_logo.png +0 -0
  10. data/app/edamame_san/public/images/edamame_logo_2.icns +0 -0
  11. data/app/edamame_san/public/javascripts/application.js +8 -0
  12. data/app/edamame_san/public/javascripts/jquery/jquery-ui.js +8694 -0
  13. data/app/edamame_san/public/javascripts/jquery/jquery.js +4376 -0
  14. data/app/edamame_san/public/stylesheets/application.css +32 -0
  15. data/app/edamame_san/public/stylesheets/layout.css +88 -0
  16. data/app/edamame_san/views/layout.haml +13 -0
  17. data/app/edamame_san/views/load.haml +37 -0
  18. data/app/edamame_san/views/root.haml +25 -0
  19. data/bin/edamame-nuke +20 -0
  20. data/bin/edamame-ps +2 -0
  21. data/bin/edamame-stats +13 -0
  22. data/bin/edamame-sync +21 -0
  23. data/bin/edamame_util_opts.rb +10 -0
  24. data/bin/test_run.rb +14 -0
  25. data/lib/edamame.rb +29 -0
  26. data/lib/edamame/broker.rb +38 -0
  27. data/lib/edamame/job.rb +114 -0
  28. data/lib/edamame/monitoring.rb +7 -0
  29. data/lib/edamame/monitoring/README-god.textile +54 -0
  30. data/lib/edamame/monitoring/beanstalkd_god.rb +28 -0
  31. data/lib/edamame/monitoring/god_email.rb +45 -0
  32. data/lib/edamame/monitoring/god_process.rb +205 -0
  33. data/lib/edamame/monitoring/process_groups.rb +32 -0
  34. data/lib/edamame/monitoring/sinatra_god.rb +34 -0
  35. data/lib/edamame/monitoring/tyrant_god.rb +59 -0
  36. data/lib/edamame/persistent_queue.rb +152 -0
  37. data/lib/edamame/queue.rb +6 -0
  38. data/lib/edamame/queue/beanstalk.rb +134 -0
  39. data/lib/edamame/scheduling.rb +79 -0
  40. data/lib/edamame/store.rb +8 -0
  41. data/lib/edamame/store/base.rb +62 -0
  42. data/lib/edamame/store/tyrant_store.rb +49 -0
  43. data/lib/methods.txt +94 -0
  44. data/spec/edamame_spec.rb +7 -0
  45. data/spec/spec_helper.rb +10 -0
  46. data/utils/god/edamame.god +36 -0
  47. data/utils/god/edamame.yaml +61 -0
  48. data/utils/god/god-etc-init-dot-d-example +40 -0
  49. data/utils/god/god.conf +22 -0
  50. data/utils/god/god_site_config.rb +4 -0
  51. data/utils/god/wuclan.god +36 -0
  52. data/utils/simulation/Add Percent Variation.vi +0 -0
  53. data/utils/simulation/Harmonic Average.vi +0 -0
  54. data/utils/simulation/Rescheduling Simulation.aliases +3 -0
  55. data/utils/simulation/Rescheduling Simulation.lvlps +3 -0
  56. data/utils/simulation/Rescheduling Simulation.lvproj +22 -0
  57. data/utils/simulation/Rescheduling.vi +0 -0
  58. data/utils/simulation/Weighted Average.vi +0 -0
  59. 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,6 @@
1
+ module Edamame
2
+ module Queue
3
+ extend FactoryModule
4
+ autoload :BeanstalkQueue, 'edamame/queue/beanstalk'
5
+ end
6
+ 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,8 @@
1
+ require 'monkeyshines/utils/factory_module'
2
+ module Edamame
3
+ module Store
4
+ extend FactoryModule
5
+ autoload :Base, 'edamame/store/base'
6
+ autoload :TyrantStore, 'edamame/store/tyrant_store'
7
+ end
8
+ 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
+