hot_potato 0.12.1

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 (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
@@ -0,0 +1,172 @@
1
+ module HotPotato
2
+
3
+ # The routes file (config/routes.rb) is a Ruby DSL that does the following:
4
+ #
5
+ # * Defines AppTasks (Faucets, Workers, Sinks)
6
+ # * Defines processing chain for AppTasks
7
+ # * Restrict AppTasks to a host group
8
+ # * Limit number of instances
9
+ #
10
+ # Example:
11
+ #
12
+ # HotPotato::Route.build do
13
+ #
14
+ # faucet :twitter_faucet
15
+ # worker :influencer, :source => :twitter_faucet
16
+ # sink :log_writer, :source => :influencer
17
+ #
18
+ # end
19
+ #
20
+ # Multiple sources can be attached to a worker or sink:
21
+ #
22
+ # worker :influencer, :source => [:twitter_faucet. :other_source]
23
+ #
24
+ # The number of instances is set to 1. This can be changed by setting the number of instances:
25
+ #
26
+ # worker :influencer, :source => :twitter_faucet, :instances => 2
27
+ #
28
+ # AppTasks can be limited to a specific server (or set of servers) by creating a group in the
29
+ # config/config.yml file:
30
+ #
31
+ # development:
32
+ # redis_hostname: localhost
33
+ # redis_port: 6379
34
+ # servers:
35
+ # - hostname: worker01
36
+ # group: incoming
37
+ # max_app_tasks: 15
38
+ # - hostname: worker02
39
+ # group: worker
40
+ # max_app_tasks: 15
41
+ #
42
+ # and specifying the group in the routes files:
43
+ #
44
+ # faucet :twitter_faucet, :group => :incoming
45
+ module Route
46
+
47
+ def self.build(&block)
48
+ @@routes = Routes.new
49
+ @@routes.instance_eval(&block)
50
+ end
51
+
52
+ def self.routes
53
+ @@routes
54
+ end
55
+
56
+ class Routes
57
+
58
+ attr_accessor :faucets, :workers, :sinks
59
+
60
+ def initialize
61
+ @faucets = []
62
+ @workers = []
63
+ @sinks = []
64
+ end
65
+
66
+ def find(name)
67
+ app_tasks.each do |app_task|
68
+ if app_task.classname.to_s == name.to_s
69
+ return app_task
70
+ end
71
+ end
72
+ return nil
73
+ end
74
+
75
+ def app_tasks
76
+ @faucets + @workers + @sinks
77
+ end
78
+
79
+ def faucet(classname, options = {}, &block)
80
+ f = Faucet.new(classname, options)
81
+ f.instance_eval(&block) if block
82
+ @faucets << f
83
+ end
84
+
85
+ def worker(classname, options = {}, &block)
86
+ w = Worker.new(classname, options)
87
+ w.instance_eval(&block) if block
88
+ @workers << w
89
+ end
90
+
91
+ def sink(classname, options = {}, &block)
92
+ s = Sink.new(classname, options)
93
+ s.instance_eval(&block) if block
94
+ @sinks << s
95
+ end
96
+
97
+ def to_s
98
+ str = "Faucets\n"
99
+ @faucets.each do |f|
100
+ str << f.to_s
101
+ end
102
+ str << "Workers\n"
103
+ @workers.each do |f|
104
+ str << f.to_s
105
+ end
106
+ str << "Sinks\n"
107
+ @sinks.each do |f|
108
+ str << f.to_s
109
+ end
110
+ str
111
+ end
112
+ end
113
+
114
+ class AppTask
115
+
116
+ include HotPotato::Core
117
+
118
+ attr_accessor :classname, :instances, :source, :group
119
+ def initialize(classname, options = {})
120
+ @classname = classname.to_s
121
+ @instances = options[:instances] || 1
122
+ @group = options[:group] || ""
123
+ if options[:source]
124
+ if options[:source].class == Array
125
+ @source = options[:source].map { |s| s.to_s }
126
+ else
127
+ @source = [options[:source].to_s]
128
+ end
129
+ else
130
+ @source = nil
131
+ end
132
+ end
133
+
134
+ def allow_group(name)
135
+ return true if @group == ""
136
+ return name == @group.to_s
137
+ end
138
+
139
+ def running_instances
140
+ stat.keys("hotpotato.apptask.*.#{classify(@classname)}.*").count || 0
141
+ end
142
+
143
+ def type
144
+ return "Faucet" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Faucet)
145
+ return "Worker" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Worker)
146
+ return "Sink" if Kernel.const_get(@data[:classname]).ancestors.include?(HotPotato::Sink)
147
+ return "AppTask"
148
+ end
149
+
150
+ end
151
+
152
+ class Faucet < AppTask
153
+ def to_s
154
+ " Faucet class [#{@classname}]\n"
155
+ end
156
+ end
157
+
158
+ class Worker < AppTask
159
+ def to_s
160
+ " Worker class [#{@classname}]\n"
161
+ end
162
+ end
163
+
164
+ class Sink < AppTask
165
+ def to_s
166
+ " Sink class [#{@classname}]\n"
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ end
@@ -0,0 +1,41 @@
1
+ module HotPotato
2
+
3
+ # Faucets inject data into the system. Examples include: Twitter Reader, SMTP, and Tail Log File.
4
+ # Each faucet is a ruby file in the app directory that extends HotPotato::Faucet and implements
5
+ # the perform method. For each message received, the method should call the send_message to send
6
+ # it to the next AppTask.
7
+ #
8
+ # class TwitterFaucet < HotPotato::Faucet
9
+ #
10
+ # def perform
11
+ # TweetStream::Client.new("user", "secret").sample do |s|
12
+ # message = {}
13
+ # message["username"] = s.user.screen_name
14
+ # message["text"] = s.text
15
+ # send_message message
16
+ # end
17
+ # end
18
+ #
19
+ # end
20
+ class Faucet
21
+
22
+ include HotPotato::AppTask
23
+
24
+ def start
25
+ @queue_out = underscore(self.class.name.to_sym)
26
+ if self.respond_to?('perform')
27
+ start_heartbeat_service
28
+ perform
29
+ else
30
+ log.error "The Faucet #{self.class.name} does not implement a perform method."
31
+ end
32
+ end
33
+
34
+ def send_message(m)
35
+ queue_inject @queue_out, m
36
+ count_message_out
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,55 @@
1
+ require 'fileutils'
2
+
3
+ module HotPotato
4
+
5
+ class Generate
6
+
7
+ def initialize(app_path)
8
+ @app_path = app_path
9
+ log "Generating application #{@app_path}..."
10
+ mkdir app_path
11
+ copy_file "Gemfile"
12
+ copy_file "Rakefile"
13
+ mkdir "app"
14
+ mkdir "bin"
15
+ copy_file "admin", 'bin'
16
+ copy_file "app_task", 'bin'
17
+ copy_file "generate", 'bin'
18
+ copy_file "supervisor", 'bin'
19
+ mkdir "config"
20
+ mkdir "config/environments"
21
+ copy_file "development.rb", 'config/environments'
22
+ copy_file "test.rb", 'config/environments'
23
+ copy_file "production.rb", 'config/environments'
24
+ copy_file "boot.rb", 'config'
25
+ copy_file "config.yml", 'config'
26
+ copy_file "routes.rb", 'config'
27
+ mkdir "docs"
28
+ mkdir "logs"
29
+ mkdir "test"
30
+ mkdir "tmp"
31
+ end
32
+
33
+ def mkdir(path)
34
+ dir = path == @app_path ? @app_path : "#{@app_path}/#{path}"
35
+ if Dir.exists?(dir)
36
+ log " exist #{dir}"
37
+ else
38
+ Dir.mkdir(dir)
39
+ log " create #{dir}"
40
+ end
41
+ end
42
+
43
+ def copy_file(src, dest = "")
44
+ dest = "#{dest}/" unless dest == ""
45
+ log " add #{@app_path}/#{dest}#{src}"
46
+ FileUtils.cp "#{File.expand_path('..', __FILE__)}/templates/#{src}", "#{@app_path}/#{dest}#{src}"
47
+ end
48
+
49
+ def log(message)
50
+ puts "#{message}"
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,41 @@
1
+ module HotPotato
2
+
3
+ class GenerateAppTask
4
+
5
+ def initialize
6
+ usage unless ARGV[1]
7
+ name = underscore(ARGV[1])
8
+
9
+ case ARGV[0]
10
+ when "sink"
11
+ process_template "template_sink.rb", "#{name}.rb", classify(name)
12
+ when "faucet"
13
+ process_template "template_faucet.rb", "#{name}.rb", classify(name)
14
+ when "worker"
15
+ process_template "template_worker.rb", "#{name}.rb", classify(name)
16
+ else
17
+ usage
18
+ end
19
+ end
20
+
21
+ def usage
22
+ puts "Usage: generate [faucet|worker|sink] name"
23
+ exit 1
24
+ end
25
+
26
+ def process_template(src, dest, name)
27
+
28
+ template_file = File.open("#{File.expand_path('..', __FILE__)}/templates/#{src}")
29
+ contents = ""
30
+ template_file.each { |line| contents << line}
31
+
32
+ result = contents.gsub("__NAME__", name)
33
+ dest_file = File.new("#{APP_PATH}/app/#{dest}", "w")
34
+ dest_file.write(result)
35
+ dest_file.close
36
+ puts "Writing #{APP_PATH}/app/#{dest}"
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,33 @@
1
+ module HotPotato
2
+
3
+ class QueueLogger
4
+
5
+ include HotPotato::Core
6
+
7
+ attr_accessor :level, :formatter
8
+
9
+ def initialize(options = {})
10
+ @classname = options[:classname]
11
+ end
12
+
13
+ def log(message, severity)
14
+ log_entry = {}
15
+ log_entry[:created_at] = Time.now
16
+ log_entry[:message] = message
17
+ log_entry[:severity] = severity
18
+ log_entry[:host] = Socket.gethostname
19
+ log_entry[:pid] = Process.pid
20
+ log_entry[:classname] = @classname
21
+
22
+ queue_inject "hotpotato.log.#{log_entry[:host]}", log_entry.to_json
23
+ end
24
+
25
+ def debug(m); log m, __method__; end
26
+ def info(m); log m, __method__; end
27
+ def warn(m); log m, __method__; end
28
+ def error(m); log m, __method__; end
29
+ def fatal(m); log m, __method__; end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,33 @@
1
+ module HotPotato
2
+
3
+ # Sinks send data out of the system. Examples include: WebSocket, Database (Data Warehouse), and
4
+ # File Writer. Each sink is a ruby file in the app directory that extends HotPotato::Sink and implements
5
+ # the perform(message) method. There is no send_message for the sink to call since it is a final destination
6
+ # for the message.
7
+ #
8
+ # class LogWriter < HotPotato::Sink
9
+ #
10
+ # def perform(message)
11
+ # log.debug "#{message["username"]}:#{message["influence"]}"
12
+ # end
13
+ #
14
+ # end
15
+ class Sink
16
+
17
+ include HotPotato::AppTask
18
+
19
+ def start(queue_in)
20
+ if !self.respond_to?('perform')
21
+ log.error "The Sink #{self.class.name} does not implement a perform method."
22
+ exit 1
23
+ end
24
+ start_heartbeat_service
25
+ queue_subscribe(queue_in) do |m|
26
+ count_message_in
27
+ perform m
28
+ end
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,64 @@
1
+ require "socket"
2
+
3
+ class SupervisorInfo
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[:pid] = options[:pid] || Process.pid
13
+ end
14
+
15
+ def key
16
+ "hotpotato.supervisor.#{@data[:hostname]}.#{@data[:pid]}"
17
+ end
18
+
19
+ def to_s
20
+ @data.to_s
21
+ end
22
+
23
+ def app_tasks
24
+ stat.keys("hotpotato.apptask.#{@data[:hostname]}.*").count
25
+ end
26
+
27
+ def hostname
28
+ @data[:hostname]
29
+ end
30
+
31
+ def pid
32
+ @data[:pid]
33
+ end
34
+
35
+ def started_at
36
+ return nil unless @data[:started_at]
37
+ DateTime.parse(@data[:started_at])
38
+ end
39
+
40
+ def updated_at
41
+ return nil unless @data[:updated_at]
42
+ DateTime.parse(@data[:updated_at])
43
+ end
44
+
45
+ def touch
46
+ @data[:updated_at] = Time.now
47
+ end
48
+
49
+ def to_json(*a)
50
+ result = @data
51
+ result["json_class"] = self.class.name
52
+ result.to_json(*a)
53
+ end
54
+
55
+ def self.json_create(o)
56
+ options = {}
57
+ options[:started_at] = o["started_at"]
58
+ options[:updated_at] = o["updated_at"]
59
+ options[:pid] = o["pid"]
60
+ options[:hostname] = o["hostname"]
61
+ new options
62
+ end
63
+
64
+ end
@@ -0,0 +1,225 @@
1
+ require 'ostruct'
2
+ require 'optparse'
3
+ require 'socket'
4
+
5
+ module HotPotato
6
+
7
+ # The supervisor is a process that runs on each machine that participates in the cluster.
8
+ # When it starts it does the following:
9
+ #
10
+ # 0. Read the routes file
11
+ # 1. Connect to the Redis server and get the appTask process ID table
12
+ # 2. Acquire the global lock
13
+ # 3. If a process is needed, fork a new process for AppTask
14
+ # 4. Release the global lock
15
+ # 5. Rinse and Repeat
16
+ #
17
+ # The supervisor also starts the Heartbeat service and logging service as background threads.
18
+ #
19
+ # The supervisor can be managed from the command line:
20
+ #
21
+ # $ bin/supervisor [run|start|stop|restart]
22
+ #
23
+ # If started without any settings, it will default to run.
24
+ class SupervisorServer
25
+
26
+ include HotPotato::Core
27
+
28
+ MAX_APP_TASKS = 32
29
+ HEARTBEAT_INTERVAL = 20
30
+ PID_FILE = "#{APP_PATH}/tmp/supervisor.pid"
31
+ LOG_FILE = "#{APP_PATH}/logs/supervisor.log"
32
+
33
+ def initialize
34
+ @options = load_options
35
+ @options.mode = parse_options
36
+ trap("INT") { shutdown }
37
+ self.send(@options.mode)
38
+ end
39
+
40
+ def run
41
+ $0 = "Hot Potato Supervisor"
42
+ log.info "Starting Hot Potato Supervisor #{HotPotato::VERSION}"
43
+ begin
44
+ start_heartbeat_service
45
+ start_log_service
46
+ routes = HotPotato::Route.routes
47
+ while @options.running do
48
+ if acquire_lock :supervisor
49
+ log.debug "Lock acquired"
50
+ routes.app_tasks.each do |app_task|
51
+ if app_task.running_instances < app_task.instances && app_task.allow_group(@options.group)
52
+ if has_capacity
53
+ log.info "Starting AppTask [#{classify(app_task.classname)}]"
54
+ pid = fork do
55
+ Process.setsid
56
+ exec "#{APP_PATH}/bin/app_task #{app_task.classname.to_s}"
57
+ end
58
+ Process.detach pid
59
+ sleep 2
60
+ else
61
+ log.warn "Cannot start AppTask [#{app_task.classname}] - Server at Capacity (Increase max_app_tasks)"
62
+ end
63
+ end
64
+ end
65
+ release_lock :supervisor
66
+ end
67
+ sleep (5 + rand(5))
68
+ end
69
+ rescue Exception
70
+ log.error $!
71
+ log.error $@
72
+ exit 1
73
+ end
74
+ end
75
+
76
+ def start
77
+ # Check if we are running
78
+ if File.exists?(PID_FILE)
79
+ pid = 0;
80
+ File.open(PID_FILE, 'r') do |f|
81
+ pid = f.read.to_i
82
+ end
83
+ # Check if we are REALLY running
84
+ if Process.alive?(pid)
85
+ log.fatal "Supervisor is already running on this machine. Only one instance can run per machine."
86
+ exit 1
87
+ else
88
+ log.info "Supervisor is not running despite the presence of the pid file. I will overwrite the pid file and start the supervisor."
89
+ end
90
+ end
91
+ Process.daemon
92
+ File.open(PID_FILE, 'w') do |f|
93
+ f.write "#{Process.pid}\n"
94
+ end
95
+ STDIN.reopen '/dev/null'
96
+ STDOUT.reopen LOG_FILE, 'a'
97
+ STDERR.reopen STDOUT
98
+ STDOUT.sync = true
99
+ STDERR.sync = true
100
+ run
101
+ end
102
+
103
+ # Stops the Supervisor. Requires the existance of a PID file.
104
+ # Calls the shutdown hook to stop AppTasks.
105
+ def stop
106
+ pid = 0
107
+ shutdown
108
+ if File.exists?(PID_FILE)
109
+ File.open(PID_FILE, 'r') do |f|
110
+ pid = f.read.to_i
111
+ end
112
+ Process.kill("INT", pid) if Process.alive?(pid)
113
+ File.delete(PID_FILE)
114
+ else
115
+ log.fatal "Supervisor PID file does not exist."
116
+ exit 1
117
+ end
118
+ end
119
+
120
+ # Restarts the Supervisor
121
+ def restart
122
+ stop
123
+ sleep 2
124
+ start
125
+ end
126
+
127
+ def parse_options
128
+ mode = :run
129
+ op = OptionParser.new do |opts|
130
+ opts.banner = "Usage: #{$0} [run|start|stop|restart]"
131
+ opts.on_tail("-h", "--help", "Show this message") do
132
+ puts op
133
+ exit
134
+ end
135
+ end
136
+ begin
137
+ op.parse!
138
+ mode = (ARGV.shift || "run").to_sym
139
+ if ![:start, :stop, :restart, :run].include?(mode)
140
+ puts op
141
+ exit 1
142
+ end
143
+ rescue
144
+ puts op
145
+ exit 1
146
+ end
147
+ return mode
148
+ end
149
+
150
+ # Kills any running AppTasks on this machine and removes entries from the
151
+ # process table cache. Removes entry for the supervisor in the process
152
+ # table cache.
153
+ def shutdown
154
+ @options.running = false
155
+ stat.keys("hotpotato.apptask.#{@options.hostname}.*").each do |app_task_key|
156
+ app_task = JSON.parse(stat.get(app_task_key))
157
+ log.info "Killing PID #{app_task.pid} [#{app_task.classname}]"
158
+ Process.kill("INT", app_task.pid) if Process.alive?(app_task.pid)
159
+ stat.del app_task_key
160
+ end
161
+ stat.keys("hotpotato.supervisor.#{@options.hostname}.*").each do |supervisor_key|
162
+ stat.del supervisor_key
163
+ end
164
+ log.info "Stopping Supervisor..."
165
+ end
166
+
167
+ # Determines if this host reached the limit of the number of AppTasks it
168
+ # can run
169
+ def has_capacity
170
+ return stat.keys("hotpotato.apptask.#{@options.hostname}.*").count < @options.max_app_tasks
171
+ end
172
+
173
+ # OK, this is not really a log service, but it is responsible for subscribing to the log messages
174
+ # from the AppTasks on this server.
175
+ def start_log_service
176
+ Thread.new do
177
+ log.info "Thread created for Supervisor [Log]"
178
+ queue_subscribe("hotpotato.log.#{@options.hostname}") do |m|
179
+ log_entry = JSON.parse(m)
180
+ if log.respond_to?(log_entry["severity"].to_sym)
181
+ log.send(log_entry["severity"].to_sym, "#{log_entry['classname']} [#{log_entry['pid']}] - #{log_entry['message']}")
182
+ end
183
+ end
184
+ end
185
+ end
186
+
187
+ # Starts a background thread to update the process list in redis.
188
+ def start_heartbeat_service
189
+ si = SupervisorInfo.new
190
+ stat.set si.key, si.to_json
191
+ stat.expire si.key, 120
192
+
193
+ Thread.new do
194
+ log.info "Thread created for Supervisor [Heartbeat]"
195
+ loop do
196
+ si.touch
197
+ stat.set si.key, si.to_json, 120
198
+ sleep HEARTBEAT_INTERVAL
199
+ end
200
+ end
201
+ end
202
+
203
+ # Loads the options, mainly from the config.yml file, into an OpenStruct object.
204
+ def load_options
205
+ options = OpenStruct.new
206
+ options.max_app_tasks = MAX_APP_TASKS
207
+ options.group = ""
208
+ options.mode = :run
209
+ options.hostname = Socket.gethostname
210
+ options.running = true
211
+
212
+ config["servers"].each do |server|
213
+ if server["hostname"] == options.hostname
214
+ options.max_app_tasks = server["max_app_tasks"] || MAX_APP_TASKS
215
+ options.group = server["group"] || ""
216
+ break
217
+ end
218
+ end
219
+
220
+ return options
221
+ end
222
+
223
+ end
224
+
225
+ end
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "hot_potato"
4
+ gem "redis"
5
+ gem "json"
6
+ gem "vegas"
@@ -0,0 +1,9 @@
1
+ require 'rake'
2
+ require 'rake/testtask'
3
+ require 'rake/rdoctask'
4
+ require File.expand_path('../config/boot', __FILE__)
5
+
6
+ desc 'Print the routing table'
7
+ task :routes do
8
+ puts HotPotato::Route.routes.to_s
9
+ end
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../config/boot', __FILE__)
4
+ Vegas::Runner.new(HotPotato::Admin, 'Hot Potato Admin Server')
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.expand_path('../../config/boot', __FILE__)
4
+ HotPotato::AppTaskServer.new