heroku-commander 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.3.0 (04/15/2013)
2
+ ==================
3
+ * Fixed an infinite loop in the tail restart method used by `Heroku::Commander.run` with `detached: true` - [@macreery](https://github.com/macreery).
4
+ * The `Heroku::Commander.run` with `detached: true` will now restart the tail process when aborted without having received a process exit message - [@dblock](https://github.com/dblock).
5
+ * Added a `tail_retries` that defines the maximum number of tail restarts, default is 3 - [@dblock](https://github.com/dblock).
6
+ * Added a `size` option to `Heroku::Runner` and `Heroku::Commander.run`, supporting `2X` dynos - [@dblock](https://github.com/dblock).
7
+
1
8
  0.2.0 (02/14/2013)
2
9
  ==================
3
10
 
data/README.md CHANGED
@@ -47,6 +47,12 @@ commander = Heroku::Commander.new({ :app => "heroku-commander" })
47
47
  commander.run "uname -a" # => [ "Linux 2.6.32-348-ec2 #54-Ubuntu SMP x86_64 GNU" ]
48
48
  ```
49
49
 
50
+ You can specify the dyno size with `size`.
51
+
52
+ ``` ruby
53
+ commander.run "uname -a", { size: "2X" }
54
+ ```
55
+
50
56
  Heroku Detached Run
51
57
  -------------------
52
58
 
@@ -67,7 +73,9 @@ end
67
73
 
68
74
  You can pass the following options along with `:detached`:
69
75
 
70
- * **tail_timeout**: number of seconds to wait before terminating `heroku logs --tail`, expecting more output (default to 5).
76
+ * **size**: dyno size, eg. `2X` for double-dynos.
77
+ * **tail_timeout**: number of seconds to wait before terminating `heroku logs --tail`, expecting more output (defaults to 5).
78
+ * **tail_retries**: number of times to restart the tail process on error (defaults to 3).
71
79
 
72
80
  For more information about Heroku one-off dynos see [this documentation](https://devcenter.heroku.com/articles/one-off-dynos).
73
81
 
@@ -7,7 +7,7 @@ logger = Logger.new($stdout)
7
7
  logger.level = Logger::DEBUG
8
8
  commander = Heroku::Commander.new({ :logger => logger })
9
9
 
10
- uname = commander.run "uname -a"
10
+ uname = commander.run "uname -a", { :size => "2X" }
11
11
  logger.info "Heroku dyno is a #{uname.join('\n')}."
12
12
 
13
13
  files = []
@@ -33,3 +33,7 @@ en:
33
33
  message: "The process `%{pid}` does not exist."
34
34
  summary: "The output of heroku ps did not list the requested process."
35
35
  resolution: "Verify that the process is running."
36
+ invalid_option:
37
+ message: "Invalid option `%{name}`."
38
+ summary: "The value of the option is invalid: `%{value}`. Must be %{range}."
39
+ resolution: "Refer to the documentation for acceptable values."
@@ -34,7 +34,8 @@ module Heroku
34
34
 
35
35
  # Run a process synchronously
36
36
  def run(command, options = {}, &block)
37
- runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command })
37
+ size = options.delete(:size) if options
38
+ runner = Heroku::Runner.new({ :app => app, :logger => logger, :command => command, size: size })
38
39
  runner.run!(options, &block)
39
40
  end
40
41
 
@@ -6,3 +6,4 @@ require 'heroku/commander/errors/missing_pid_error'
6
6
  require 'heroku/commander/errors/unexpected_output_error'
7
7
  require 'heroku/commander/errors/already_running_error'
8
8
  require 'heroku/commander/errors/no_such_process_error'
9
+ require 'heroku/commander/errors/invalid_option_error'
@@ -0,0 +1,13 @@
1
+ module Heroku
2
+ class Commander
3
+ module Errors
4
+ class InvalidOptionError < Heroku::Commander::Errors::Base
5
+
6
+ def initialize(opts = {})
7
+ super(compose_message("invalid_option", opts))
8
+ end
9
+
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,5 +1,5 @@
1
1
  module Heroku
2
2
  class Commander
3
- VERSION = '0.2.0'
3
+ VERSION = '0.3.0'
4
4
  end
5
5
  end
data/lib/heroku/runner.rb CHANGED
@@ -1,13 +1,14 @@
1
1
  module Heroku
2
2
  class Runner
3
3
 
4
- attr_accessor :app, :logger, :command
4
+ attr_accessor :app, :logger, :command, :size
5
5
  attr_reader :pid, :running, :tail
6
6
 
7
7
  def initialize(options = {})
8
8
  @app = options[:app]
9
9
  @logger = options[:logger]
10
10
  @command = options[:command]
11
+ @size = options[:size]
11
12
  raise Heroku::Commander::Errors::MissingCommandError unless @command
12
13
  end
13
14
 
@@ -58,7 +59,12 @@ module Heroku
58
59
  end
59
60
 
60
61
  def cmdline(options = {})
61
- [ "heroku", options[:detached] ? "run:detached" : "run", "\"(#{command} 2>&1 ; echo rc=\\$?)\"", @app ? "--app #{@app}" : nil ].compact.join(" ")
62
+ [
63
+ "heroku", options[:detached] ? "run:detached" : "run",
64
+ @size ? "--size #{@size}" : nil,
65
+ "\"(#{command} 2>&1 ; echo rc=\\$?)\"",
66
+ @app ? "--app #{@app}" : nil
67
+ ].compact.join(" ")
62
68
  end
63
69
 
64
70
  def check_exit_status!(lines)
@@ -84,24 +90,61 @@ module Heroku
84
90
  lines = []
85
91
  tail_cmdline = [ "heroku", "logs", "-p #{@pid}", "--tail", @app ? "--app #{@app}" : nil ].compact.join(" ")
86
92
  previous_line = nil # delay by 1 to avoid rc=status lines
87
- Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
88
- line ||= ""
89
- # remove any ANSI output
90
- line = line.gsub /\e\[(\d+)m/, ''
91
- # lines are returned as [date/time] app/heroku[pid]: output
92
- if (line_after_prefix = line.split("[#{@pid}]:")[-1])
93
- line = line_after_prefix.strip
94
- end
95
- if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
96
- # ignore
97
- elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
98
- terminate_executor!(options[:tail_timeout] || 5)
99
- else
100
- if block_given?
101
- yield previous_line if previous_line
102
- previous_line = line
93
+ process_completed = false
94
+ # tail retries
95
+ tail_retries_left = (options[:tail_retries] || 3).to_i
96
+ if tail_retries_left < 0
97
+ raise Heroku::Commander::Errors::InvalidOptionError.new({
98
+ :name => "tail_retries",
99
+ :value => options[:tail_retries],
100
+ :range => "greater or equal to 0"
101
+ })
102
+ end
103
+ # tail timeout
104
+ tail_timeout = (options[:tail_timeout] || 5).to_i
105
+ if tail_timeout < 0
106
+ raise Heroku::Commander::Errors::InvalidOptionError.new({
107
+ :name => "tail_timeout",
108
+ :value => options[:tail_timeout],
109
+ :range => "greater or equal to 0"
110
+ })
111
+ end
112
+ # tail
113
+ process_completed = false
114
+ while ! process_completed
115
+ tail_retries_left -= 1
116
+ begin
117
+ Heroku::Executor.run tail_cmdline, { :logger => logger } do |line|
118
+ line ||= ""
119
+ # remove any ANSI output
120
+ line = line.gsub /\e\[(\d+)m/, ''
121
+ # lines are returned as [date/time] app/heroku[pid]: output
122
+ if (line_after_prefix = line.split("[#{@pid}]:")[-1])
123
+ line = line_after_prefix.strip
124
+ end
125
+ if line.match(/Starting process with command/) || line.match(/State changed from \w+ to up/)
126
+ # ignore
127
+ elsif line.match(/State changed from \w+ to complete/) || line.match(/Process exited with status \d+/)
128
+ process_completed = true
129
+ terminate_executor!(options[:tail_timeout] || 5)
130
+ else
131
+ if block_given?
132
+ yield previous_line if previous_line
133
+ previous_line = line
134
+ end
135
+ lines << line
136
+ end
137
+ end
138
+ rescue
139
+ @running = false
140
+ raise if tail_retries_left <= 0
141
+ ensure
142
+ if tail_retries_left <= 0
143
+ @running = false
144
+ raise
145
+ elsif !process_completed
146
+ logger.debug "Restarting #{tail_cmdline}, #{tail_retries_left} #{tail_retries_left == 1 ? 'retry' : 'retries'} left." if logger
103
147
  end
104
- lines << line
105
148
  end
106
149
  end
107
150
  lines
@@ -76,6 +76,15 @@ describe Heroku::Commander do
76
76
  Heroku::Runner.any_instance.should_receive(:terminate_executor!).with(42).twice
77
77
  subject.run("ls -1", { :detached => true, :tail_timeout => 42 }).should == [ "bin", "app" ]
78
78
  end
79
+ it "passes size option" do
80
+ Heroku::Executor.stub(:run).with("heroku run --size 2X \"(ls -1 2>&1 ; echo rc=\\$?)\"", { :logger => nil }).
81
+ and_yield("Running `...` attached to terminal... up, run.1234").
82
+ and_yield("app").
83
+ and_yield("bin").
84
+ and_yield("rc=0").
85
+ and_return([ "Running `...` attached to terminal... up, run.1234", "app", "bin", "rc=0" ])
86
+ subject.run("ls -1", { size: "2X" }).should == [ "app", "bin" ]
87
+ end
79
88
  end
80
89
  context "processes" do
81
90
  context "without processes" do
@@ -103,6 +103,88 @@ describe Heroku::Runner do
103
103
  subject.should_not be_running
104
104
  end
105
105
  end
106
+ context "tail_retries" do
107
+ before :each do
108
+ Heroku::Executor.stub(:run).with(subject.send(:cmdline, { :detached => true }), { :logger => nil }).
109
+ and_yield("Running `ls -1` detached... up, run.8748").
110
+ and_yield("Use `heroku logs -p run.8748` to view the output.").
111
+ and_yield("rc=0").
112
+ and_return([ "Running `ls -1` detached... up, run.8748", "Use `heroku logs -p run.8748` to view the output.", "rc=0" ])
113
+ # first iteration
114
+ Heroku::Executor.should_receive(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
115
+ and_yield("2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`").
116
+ and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: bin").
117
+ and_return([
118
+ "2013-01-31T01:39:30+00:00 heroku[run.8748]: Starting process with command `ls -1`",
119
+ "2013-01-31T01:39:31+00:00 app[run.8748]: bin",
120
+ ])
121
+ end
122
+ context "with a successful second iteration" do
123
+ before :each do
124
+ # second iteration
125
+ Heroku::Executor.should_receive(:run).with("heroku logs -p run.8748 --tail", { :logger => nil }).
126
+ and_yield("2013-01-31T01:39:31+00:00 app[run.8748]: app").
127
+ and_yield(nil).
128
+ and_yield("2013-01-31T00:56:13+00:00 app[run.8748]: rc=0").
129
+ and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0").
130
+ and_yield("2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete").
131
+ and_return([
132
+ "2013-01-31T01:39:31+00:00 app[run.8748]: app",
133
+ "2013-01-31T00:56:13+00:00 app[run.8748]: rc=0",
134
+ "2013-01-31T01:39:33+00:00 heroku[run.8748]: Process exited with status 0",
135
+ "2013-01-31T01:39:33+00:00 heroku[run.8748]: State changed from up to complete"
136
+ ])
137
+ Heroku::Runner.any_instance.should_receive(:terminate_executor!).twice
138
+ end
139
+ it "restarts tailer" do
140
+ lines = []
141
+ subject.run!({ :detached => true }).each do |line|
142
+ lines << line
143
+ end
144
+ lines.should == [ "bin", "app", "" ]
145
+ subject.pid.should == "run.8748"
146
+ subject.should_not be_running
147
+ end
148
+ end
149
+ context "with a caught exception on second iteration" do
150
+ before :each do
151
+ # second iteration that raises a caught exception
152
+ Heroku::Executor.should_receive(:run).exactly(2).times.with("heroku logs -p run.8748 --tail", { :logger => nil }).
153
+ and_return(nil)
154
+ end
155
+ it "raises exception" do
156
+ lines = []
157
+ expect {
158
+ subject.run!({ :detached => true }).each do |line|
159
+ lines << line
160
+ end
161
+ }.to raise_error RuntimeError
162
+ subject.should_not be_running
163
+ end
164
+ end
165
+ end
166
+ context "options" do
167
+ before :each do
168
+ Heroku::Executor.stub(:run).
169
+ and_yield("Running `ls -1` detached... up, run.8748").
170
+ and_return([])
171
+ end
172
+ it "raises an error for an invalid tail_timeout option" do
173
+ expect {
174
+ subject.run!({ :detached => true, :tail_timeout => -1 })
175
+ }.to raise_error Heroku::Commander::Errors::InvalidOptionError, /Invalid option `tail_timeout`./
176
+ end
177
+ it "raises an error for an invalid tail_retries option" do
178
+ expect {
179
+ subject.run!({ :detached => true, :tail_retries => -1 })
180
+ }.to raise_error Heroku::Commander::Errors::InvalidOptionError, /Invalid option `tail_retries`./
181
+ end
182
+ end
183
+ end
184
+ context "with size" do
185
+ subject do
186
+ Heroku::Runner.new({ :command => "ls -1", size: "2X" })
187
+ end
188
+ its(:cmdline) { should eq "heroku run --size 2X \"(ls -1 2>&1 ; echo rc=\\$?)\"" }
106
189
  end
107
190
  end
108
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heroku-commander
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-02-14 00:00:00.000000000 Z
13
+ date: 2013-04-15 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: i18n
@@ -57,6 +57,7 @@ files:
57
57
  - lib/heroku/commander/errors/base.rb
58
58
  - lib/heroku/commander/errors/client_eio_error.rb
59
59
  - lib/heroku/commander/errors/command_error.rb
60
+ - lib/heroku/commander/errors/invalid_option_error.rb
60
61
  - lib/heroku/commander/errors/missing_command_error.rb
61
62
  - lib/heroku/commander/errors/missing_pid_error.rb
62
63
  - lib/heroku/commander/errors/no_such_process_error.rb
@@ -90,7 +91,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
91
  version: '0'
91
92
  segments:
92
93
  - 0
93
- hash: -3503047870647109606
94
+ hash: 3343082937396916843
94
95
  required_rubygems_version: !ruby/object:Gem::Requirement
95
96
  none: false
96
97
  requirements: