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