hot_potato 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +19 -0
  4. data/Rakefile +25 -0
  5. data/bin/hotpotato +14 -0
  6. data/hot_potato.gemspec +28 -0
  7. data/lib/hot_potato/admin/public/admin.css +30 -0
  8. data/lib/hot_potato/admin/views/index.erb +58 -0
  9. data/lib/hot_potato/admin.rb +67 -0
  10. data/lib/hot_potato/app_task.rb +42 -0
  11. data/lib/hot_potato/app_task_info.rb +80 -0
  12. data/lib/hot_potato/app_task_server.rb +92 -0
  13. data/lib/hot_potato/cache.rb +45 -0
  14. data/lib/hot_potato/core.rb +62 -0
  15. data/lib/hot_potato/dsl.rb +172 -0
  16. data/lib/hot_potato/faucet.rb +41 -0
  17. data/lib/hot_potato/generate.rb +55 -0
  18. data/lib/hot_potato/generate_app_task.rb +41 -0
  19. data/lib/hot_potato/queue_logger.rb +33 -0
  20. data/lib/hot_potato/sink.rb +33 -0
  21. data/lib/hot_potato/supervisor_info.rb +64 -0
  22. data/lib/hot_potato/supervisor_server.rb +225 -0
  23. data/lib/hot_potato/templates/Gemfile +6 -0
  24. data/lib/hot_potato/templates/Rakefile +9 -0
  25. data/lib/hot_potato/templates/admin +4 -0
  26. data/lib/hot_potato/templates/app_task +4 -0
  27. data/lib/hot_potato/templates/boot.rb +21 -0
  28. data/lib/hot_potato/templates/config.yml +11 -0
  29. data/lib/hot_potato/templates/development.rb +0 -0
  30. data/lib/hot_potato/templates/generate +4 -0
  31. data/lib/hot_potato/templates/production.rb +0 -0
  32. data/lib/hot_potato/templates/routes.rb +3 -0
  33. data/lib/hot_potato/templates/supervisor +4 -0
  34. data/lib/hot_potato/templates/template_faucet.rb +8 -0
  35. data/lib/hot_potato/templates/template_sink.rb +7 -0
  36. data/lib/hot_potato/templates/template_worker.rb +8 -0
  37. data/lib/hot_potato/templates/test.rb +0 -0
  38. data/lib/hot_potato/utils.rb +43 -0
  39. data/lib/hot_potato/version.rb +3 -0
  40. data/lib/hot_potato/worker.rb +40 -0
  41. data/lib/hot_potato.rb +20 -0
  42. data/readme.md +219 -0
  43. data/test/helper.rb +7 -0
  44. data/test/version_test.rb +9 -0
  45. metadata +166 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in hot_potato.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010-2011 Darian Shimy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,25 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rake/rdoctask'
7
+
8
+ desc 'Default: run unit tests.'
9
+ task :default => :test
10
+
11
+ desc 'Run the Hot Potato tests.'
12
+ Rake::TestTask.new(:test) do |t|
13
+ t.libs << 'lib' << 'test'
14
+ t.pattern = 'test/*_test.rb'
15
+ t.verbose = true
16
+ end
17
+
18
+ desc 'Generate documentation for Hot Potato.'
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = 'Hot Potato'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
data/bin/hotpotato ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'hot_potato/version'
5
+ require 'hot_potato/generate'
6
+
7
+ app_path = ARGV.first
8
+ unless app_path
9
+ puts "Usage: hotpotato [-v|--version] app_name"
10
+ exit(1)
11
+ end
12
+
13
+ puts "Hot Potato (v #{HotPotato::VERSION})"
14
+ HotPotato::Generate.new app_path
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "hot_potato/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "hot_potato"
7
+ s.version = HotPotato::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Darian Shimy"]
10
+ s.email = ["dshimy@gmail.com"]
11
+ s.homepage = "http://github.com/dshimy/hotpotato"
12
+ s.summary = %q{A Real-time Processing Framework}
13
+ s.description = %q{Hot Potato is an open source real-time processing framework written in Ruby. Originally designed to process the Twitter firehose at 3,000+ tweets per second, it has been extended to support any type of streaming data as input or output to the framework. The framework excels with applications such as, social media analysis, log processing, fraud prevention, spam detection, instant messaging, and many others that include the processing of streaming data.}
14
+
15
+ s.add_dependency 'json'
16
+ s.add_dependency 'redis'
17
+ s.add_dependency 'bunny'
18
+ s.add_dependency 'sinatra'
19
+ s.add_dependency "vegas"
20
+ s.add_dependency "snappy"
21
+
22
+ s.rubyforge_project = "hot_potato"
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
26
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
27
+ s.require_paths = ["lib"]
28
+ end
@@ -0,0 +1,30 @@
1
+ body {
2
+ font-family: helvetica, sans-serif;
3
+ font-size: 12px;
4
+ }
5
+ h1 {
6
+ margin: 0 0 20px 0;
7
+ font-size: 24px;
8
+ }
9
+ h2 {
10
+ margin: 0 0 5px 0;
11
+ font-size: 20px;
12
+ }
13
+ table {
14
+ width: 900px;
15
+ border: 1px solid #ccc;
16
+ border-collapse: collapse;
17
+ margin-bottom: 20px;
18
+ }
19
+ table tr th {
20
+ border: 1px solid #ccc;
21
+ background: #EC7800;
22
+ color: #fff;
23
+ padding: 4px;
24
+
25
+ }
26
+ table tr td {
27
+ border: 1px solid #ccc;
28
+ padding: 2px 8px;
29
+ text-align: center;
30
+ }
@@ -0,0 +1,58 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
5
+ <title>Hot Potato Admin Server</title>
6
+ <link rel='stylesheet' href='/admin.css' type='text/css' media="screen">
7
+ <meta http-equiv="refresh" content="30">
8
+ </head>
9
+ <body>
10
+ <h1>Hot Potato Admin Server</h1>
11
+ <h2>Machines</h2>
12
+ <table>
13
+ <tr>
14
+ <th>Hostname</th>
15
+ <th>Process ID</th>
16
+ <th>Started</th>
17
+ <th>Last Heartbeat</th>
18
+ <th>AppTasks</th>
19
+ </tr>
20
+ <% @machines.each do |m| %>
21
+ <tr>
22
+ <td><%= m.hostname %></td>
23
+ <td><%= m.pid %></td>
24
+ <td><%= format_date(m.started_at) %></td>
25
+ <td><%= format_date(m.updated_at) %></td>
26
+ <td><%= m.app_tasks %></td>
27
+ </tr>
28
+ <% end %>
29
+ </table>
30
+
31
+ <h2>App Tasks</h2>
32
+ <table>
33
+ <tr>
34
+ <th>Hostname</th>
35
+ <th>Class</th>
36
+ <th>Type</th>
37
+ <th>Process ID</th>
38
+ <th>Started</th>
39
+ <th>Last Heartbeat</th>
40
+ <th>Requests In</th>
41
+ <th>Requests Out</th>
42
+ </tr>
43
+ <% @apptasks.each do |a| %>
44
+ <tr>
45
+ <td><%= a.hostname %></td>
46
+ <td><%= a.classname %></td>
47
+ <td><%= a.type %></td>
48
+ <td><%= a.pid %></td>
49
+ <td><%= format_date(a.started_at) %></td>
50
+ <td><%= format_date(a.updated_at) %></td>
51
+ <td><%= a.requests_in %></td>
52
+ <td><%= a.requests_out %></td>
53
+ </tr>
54
+ <% end %>
55
+ </table>
56
+
57
+ </body>
58
+ </html>
@@ -0,0 +1,67 @@
1
+ require 'sinatra/base'
2
+ require 'redis'
3
+ require 'erb'
4
+
5
+ module HotPotato
6
+
7
+ # The admin server is a Sinatra-based application to display statistical and diagnostic information.
8
+ #
9
+ # The admin server can be managed from the command line:
10
+ #
11
+ # $ bin/admin --help
12
+ #
13
+ # Usage: ./admin [options]
14
+ #
15
+ # Vegas options:
16
+ # -K, --kill kill the running process and exit
17
+ # -S, --status display the current running PID and URL then quit
18
+ # -s, --server SERVER serve using SERVER (thin/mongrel/webrick)
19
+ # -o, --host HOST listen on HOST (default: 0.0.0.0)
20
+ # -p, --port PORT use PORT (default: 5678)
21
+ # -x, --no-proxy ignore env proxy settings (e.g. http_proxy)
22
+ # -e, --env ENVIRONMENT use ENVIRONMENT for defaults (default: development)
23
+ # -F, --foreground don't daemonize, run in the foreground
24
+ # -L, --no-launch don't launch the browser
25
+ # -d, --debug raise the log level to :debug (default: :info)
26
+ # --app-dir APP_DIR set the app dir where files are stored (default: ~/.vegas/Hot_Potato_Admin_Server)/)
27
+ # -P, --pid-file PID_FILE set the path to the pid file (default: app_dir/Hot_Potato_Admin_Server.pid)
28
+ # --log-file LOG_FILE set the path to the log file (default: app_dir/Hot_Potato_Admin_Server.log)
29
+ # --url-file URL_FILE set the path to the URL file (default: app_dir/Hot_Potato_Admin_Server.url)
30
+ #
31
+ # Common options:
32
+ # -h, --help Show this message
33
+ # --version Show version
34
+ #
35
+ # The page can be accessed at http://localhost:5678
36
+ class Admin < Sinatra::Base
37
+
38
+ include HotPotato::Core
39
+
40
+ dir = File.dirname(File.expand_path(__FILE__))
41
+
42
+ set :views, "#{dir}/admin/views"
43
+ set :public, "#{dir}/admin/public"
44
+ set :static, true
45
+
46
+ helpers do
47
+ def format_date(d)
48
+ return "" unless d
49
+ d.strftime("%b %d, %Y %H:%M")
50
+ end
51
+ end
52
+
53
+ get "/" do
54
+ @machines = []
55
+ stat.keys('hotpotato.supervisor.*').each do |key|
56
+ @machines << JSON.parse(stat.get(key))
57
+ end
58
+ @apptasks = []
59
+ stat.keys('hotpotato.apptask.*').each do |key|
60
+ @apptasks << JSON.parse(stat.get(key))
61
+ end
62
+ erb :index
63
+ end
64
+
65
+ end
66
+
67
+ end
@@ -0,0 +1,42 @@
1
+ require "socket"
2
+
3
+ module HotPotato
4
+
5
+ # AppTasks are the controllers in the framework. The Supervisor (See below) is responsible for
6
+ # starting AppTasks. There are three types: Faucets, Workers, and Sinks.
7
+ module AppTask
8
+
9
+ HEARTBEAT_INTERVAL = 20
10
+
11
+ include HotPotato::Core
12
+
13
+ # Used to keep AppTask statistics when a message is received.
14
+ def count_message_in
15
+ stat.incr "hotpotato.counter.apptask.#{Socket.gethostname}.#{self.class.name}.#{Process.pid}.messages_in"
16
+ stat.expire "hotpotato.counter.apptask.#{Socket.gethostname}.#{self.class.name}.#{Process.pid}.messages_in", 600
17
+ end
18
+
19
+ # Used to keep AppTask statistics when a message is sent.
20
+ def count_message_out
21
+ stat.incr "hotpotato.counter.apptask.#{Socket.gethostname}.#{self.class.name}.#{Process.pid}.messages_out"
22
+ stat.expire "hotpotato.counter.apptask.#{Socket.gethostname}.#{self.class.name}.#{Process.pid}.messages_out", 600
23
+ end
24
+
25
+ # Starts the HeartBeat service to maintain the process id table.
26
+ def start_heartbeat_service
27
+ ati = AppTaskInfo.new(:classname => self.class.name)
28
+ stat.set ati.key, ati.to_json, 120
29
+
30
+ Thread.new do
31
+ log.info "Thread created for AppTask [Heartbeat]"
32
+ loop do
33
+ ati.touch
34
+ stat.set ati.key, ati.to_json, 120
35
+ sleep HEARTBEAT_INTERVAL
36
+ end
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ end
@@ -0,0 +1,80 @@
1
+ require "socket"
2
+
3
+ class AppTaskInfo
4
+
5
+ include HotPotato::Core
6
+
7
+ def initialize(options = {})
8
+ @data = {}
9
+ @data[:started_at] = options[:started_at] || Time.now
10
+ @data[:updated_at] = options[:updated_at] || @data[:started_at]
11
+ @data[:hostname] = options[:hostname] || Socket.gethostname
12
+ @data[:classname] = options[:classname] || "AppTask"
13
+ @data[:pid] = options[:pid] || Process.pid
14
+ end
15
+
16
+ def key
17
+ "hotpotato.apptask.#{@data[:hostname]}.#{@data[:classname]}.#{@data[:pid]}"
18
+ end
19
+
20
+ def to_s
21
+ @data.to_s
22
+ end
23
+
24
+ def type
25
+ return "Faucet" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Faucet)
26
+ return "Worker" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Worker)
27
+ return "Sink" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Sink)
28
+ end
29
+
30
+ def classname
31
+ @data[:classname]
32
+ end
33
+
34
+ def hostname
35
+ @data[:hostname]
36
+ end
37
+
38
+ def requests_in
39
+ stat.get("hotpotato.counter.apptask.#{@data[:hostname]}.#{@data[:classname]}.#{@data[:pid]}.messages_in") || 0
40
+ end
41
+
42
+ def requests_out
43
+ stat.get("hotpotato.counter.apptask.#{@data[:hostname]}.#{@data[:classname]}.#{@data[:pid]}.messages_out") || 0
44
+ end
45
+
46
+ def pid
47
+ @data[:pid].to_i
48
+ end
49
+
50
+ def started_at
51
+ return nil unless @data[:started_at]
52
+ DateTime.parse(@data[:started_at])
53
+ end
54
+
55
+ def updated_at
56
+ return nil unless @data[:updated_at]
57
+ DateTime.parse(@data[:updated_at])
58
+ end
59
+
60
+ def touch
61
+ @data[:updated_at] = Time.now
62
+ end
63
+
64
+ def to_json(*a)
65
+ result = @data
66
+ result["json_class"] = self.class.name
67
+ result.to_json(*a)
68
+ end
69
+
70
+ def self.json_create(o)
71
+ options = {}
72
+ options[:started_at] = o["started_at"]
73
+ options[:updated_at] = o["updated_at"]
74
+ options[:pid] = o["pid"]
75
+ options[:classname] = o["classname"]
76
+ options[:hostname] = o["hostname"]
77
+ new options
78
+ end
79
+
80
+ end
@@ -0,0 +1,92 @@
1
+ require 'ostruct'
2
+ require 'optparse'
3
+ require 'socket'
4
+
5
+ module HotPotato
6
+
7
+ # This is used internally.
8
+ class AppTaskServer
9
+
10
+ include HotPotato::Core
11
+
12
+ def initialize
13
+ @options = load_options
14
+ @options.app_task_name, @options.mode = parse_options
15
+ trap("INT") { shutdown }
16
+ self.send(@options.mode)
17
+ end
18
+
19
+ def start
20
+ set_logger(:queue_logger, :classname => classify(app_task_name))
21
+ Process.daemon
22
+ STDIN.reopen '/dev/null'
23
+ STDOUT.sync = true
24
+ STDERR.sync = true
25
+ run
26
+ end
27
+
28
+ def run
29
+ $0 = "Hot Potato AppTask [#{classify(@options.app_task_name)}]"
30
+ log.info "Starting Hot Potato AppTask #{HotPotato::VERSION} #{classify(@options.app_task_name)}"
31
+ app_task = @options.routes.find @options.app_task_name
32
+ if app_task
33
+ obj = Kernel.const_get(classify(app_task.classname)).new
34
+ begin
35
+ if app_task.source
36
+ obj.start app_task.source
37
+ else
38
+ obj.start
39
+ end
40
+ rescue Exception
41
+ log.fatal $!
42
+ exit 1
43
+ end
44
+ else
45
+ puts "Could not find #{@options.app_task_name} in the route definition"
46
+ exit 1
47
+ end
48
+ end
49
+
50
+ def parse_options
51
+ mode = :run
52
+ app_task_name = ""
53
+ op = OptionParser.new do |opts|
54
+ opts.banner = "Usage: #{$0} app_task_name [run|start]"
55
+ opts.on_tail("-h", "--help", "Show this message") do
56
+ puts op
57
+ exit!
58
+ end
59
+ end
60
+ begin
61
+ op.parse!
62
+ app_task_name = ARGV.shift
63
+ mode = (ARGV.shift || "run").to_sym
64
+ if ![:start, :run].include?(mode)
65
+ puts op
66
+ exit!
67
+ end
68
+ rescue
69
+ puts op
70
+ exit!
71
+ end
72
+ return app_task_name, mode
73
+ end
74
+
75
+ def shutdown
76
+ log.info "Stopping AppTask..."
77
+ exit!
78
+ end
79
+
80
+ def load_options
81
+ options = OpenStruct.new
82
+ options.mode = :run
83
+ options.hostname = Socket.gethostname
84
+ options.routes = HotPotato::Route.routes
85
+ options.app_task_name = ""
86
+
87
+ return options
88
+ end
89
+
90
+ end
91
+
92
+ end
@@ -0,0 +1,45 @@
1
+ module HotPotato
2
+
3
+ class Cache
4
+
5
+ include HotPotato::Core
6
+
7
+ def initialize
8
+ log.info "Initializing connection to Redis (#{config['redis_hostname']}:#{config['redis_port']})"
9
+ @@redis ||= Redis.new :host => config['redis_hostname'], :port => config['redis_port']
10
+ end
11
+
12
+ def get(k)
13
+ @@redis.get(k)
14
+ end
15
+
16
+ def set(k, v, ttl = nil)
17
+ @@redis.multi do
18
+ @@redis.set(k, v)
19
+ @@redis.expire(k, ttl) if ttl
20
+ end
21
+ end
22
+
23
+ def getset(k, v)
24
+ @@redis.getset(k, v)
25
+ end
26
+
27
+ def keys(k)
28
+ @@redis.keys(k)
29
+ end
30
+
31
+ def del(k)
32
+ @@redis.del(k)
33
+ end
34
+
35
+ def expire(k, ttl)
36
+ @@redis.expire(k, ttl)
37
+ end
38
+
39
+ def incr(k)
40
+ @@redis.incr(k)
41
+ end
42
+
43
+ end
44
+
45
+ end
@@ -0,0 +1,62 @@
1
+ require 'logger'
2
+ require 'snappy'
3
+
4
+ module HotPotato
5
+
6
+ module Core
7
+
8
+ CONFIG_PATH = "#{APP_PATH}/config/config.yml"
9
+
10
+ def set_logger(provider, options = {})
11
+ if provider == :queue_logger
12
+ @@log ||= QueueLogger.new(options)
13
+ else
14
+ @@log ||= Logger.new(STDOUT)
15
+ end
16
+ end
17
+
18
+ def log
19
+ @@log ||= Logger.new(STDOUT)
20
+ @@log.level = Logger::INFO
21
+ return @@log
22
+ end
23
+
24
+ def config
25
+ @@config ||= YAML.load_file(CONFIG_PATH)[RACK_ENV]
26
+ end
27
+
28
+ def stat
29
+ @@cache ||= Cache.new
30
+ end
31
+
32
+ def queue_inject(name, message)
33
+ @@queue ||= Redis.new :host => config['redis_hostname'], :port => config['redis_port']
34
+ @@queue.publish name.to_sym, Snappy.deflate(message.to_json)
35
+ end
36
+
37
+ def queue_subscribe(name, &block)
38
+ queue ||= Redis.new :host => config['redis_hostname'], :port => config['redis_port']
39
+ queue.subscribe(*name) do |on|
40
+ on.message do |channel, message|
41
+ yield JSON.parse(Snappy.inflate(message))
42
+ end
43
+ end
44
+ end
45
+
46
+ def acquire_lock(key, duration = 10)
47
+ if !stat.getset("hotpotato.lock.#{key.to_s}", "1")
48
+ stat.expire "hotpotato.lock.#{key.to_s}", duration
49
+ return true
50
+ else
51
+ stat.expire "hotpotato.lock.#{key.to_s}", duration
52
+ return false
53
+ end
54
+ end
55
+
56
+ def release_lock(key)
57
+ stat.del("hotpotato.lock.#{key.to_s}")
58
+ end
59
+
60
+ end
61
+
62
+ end