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
@@ -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