wrapbox 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: edc9210efdc2d7248e0b6eab05f04c89f7223b90
4
- data.tar.gz: afe62fcbfbf5f5383b12e476709fd7d79a36af27
3
+ metadata.gz: 8118b81067d91ea3535d8c3c5ca1ba55c412d345
4
+ data.tar.gz: 33fb0eb243ff53a6bc6ec7c5147a69ea3bbbc206
5
5
  SHA512:
6
- metadata.gz: 8f856b7dc58a1776c662f4bd43d1165c043c4ec8a2e5ad0a12eac1b4c60a1162d9ef57541c1d54373471f59879f6257cd8ae1105c1b26e142ffc790839694f75
7
- data.tar.gz: 0cc10bf941d6b73d320581ae722383c888f5cac1fe700bfccfd5b5ab117abd3b1347abb5f67229d8048d1ff784768224122dd8c767f7188888489b1877a0e559
6
+ metadata.gz: 5e9a9730bca6b1dddbe9a8c21a2660645dc0d3f6f3c71a74c24e175930a6bd01544e2f2b2efd6d04fc0b14bdbeeded4a4d5ad7abe38e37c4c5abe83593c1a37c
7
+ data.tar.gz: 74c5c6831768ed37e5107903426131a4158220b808b39fae6d08b74df14dcfcc2f9bd495ec1ce3f6157a66ee5602f08a5161e0c73b6535f1cefdf4ebbf14a388
data/README.md CHANGED
@@ -42,6 +42,17 @@ docker:
42
42
  memory: 1024
43
43
  ```
44
44
 
45
+ #### run by CLI
46
+
47
+ ```sh
48
+ $ wrapbox ecs run_cmd -f config.yml \
49
+ -e "FOO=bar,HOGE=fuga" \
50
+ "bundle exec rspec spec/models" \
51
+ "bundle exec rspec spec/controllers" \
52
+ ```
53
+
54
+ #### run by ruby
55
+
45
56
  ```ruby
46
57
  Wrapbox.configure do |c|
47
58
  c.load_yaml(File.expand_path("../config.yml", __FILE__))
@@ -53,10 +64,7 @@ Wrapbox.run("TestJob", :perform, ["arg1", ["arg2", "arg3"]], environments: [{nam
53
64
  Wrapbox.run("TestJob", :perform, ["arg1", ["arg2", "arg3"]], config_name: :docker, environments: [{name: "RAILS_ENV", value: "development"}]) # use docker config
54
65
 
55
66
  # runs ls . command in ECS container
56
- Wrapbox.run_cmd("ls", ".", environments: [{name: "RAILS_ENV", value: "development"}])
57
-
58
- # runs ls . command in local docker container
59
- Wrapbox.run_cmd("ls", ".", config_name: :docker, environments: [{name: "RAILS_ENV", value: "development"}])
67
+ Wrapbox.run_cmd(["ls ."], environments: [{name: "RAILS_ENV", value: "development"}])
60
68
  ```
61
69
 
62
70
  ## Config
@@ -86,9 +94,9 @@ Wrapbox.run_cmd("ls", ".", config_name: :docker, environments: [{name: "RAILS_EN
86
94
 
87
95
  ## API
88
96
 
89
- #### `run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10)`
97
+ #### `run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)`
90
98
 
91
- #### `run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10)`
99
+ #### `run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)`
92
100
 
93
101
  following options is only for ECS.
94
102
 
@@ -98,7 +106,7 @@ following options is only for ECS.
98
106
  - launch_timeout
99
107
  - launch_retry
100
108
 
101
- If Wrapbox cannot launch task in `launch_timeout` seconds, it puts custom metric data to CloudWatch.
109
+ If Wrapbox cannot create task, it puts custom metric data to CloudWatch.
102
110
  Custom metric data is `wrapbox/WaitingTaskCount` that has `ClusterName` dimension.
103
111
  And, it retry launching until retry count reach `launch_retry`.
104
112
 
@@ -29,13 +29,18 @@ module Wrapbox
29
29
  exec_docker(definition: definition, cmd: ["bundle", "exec", "rake", "wrapbox:run"], environments: envs)
30
30
  end
31
31
 
32
- def run_cmd(*cmd, container_definition_overrides: {}, environments: [])
32
+ def run_cmd(cmds, container_definition_overrides: {}, environments: [])
33
33
  definition = container_definition
34
34
  .merge(container_definition_overrides)
35
35
 
36
36
  environments = extract_environments(environments)
37
37
 
38
- exec_docker(definition: definition, cmd: cmd, environments: environments)
38
+ ths = cmds.map do |cmd|
39
+ Thread.new(cmd) do |c|
40
+ exec_docker(definition: definition, cmd: c.split(/\s+/), environments: environments)
41
+ end
42
+ end
43
+ ths.each(&:join)
39
44
  end
40
45
 
41
46
  private
@@ -67,7 +72,7 @@ module Wrapbox
67
72
 
68
73
  container = ::Docker::Container.create(options)
69
74
 
70
- container.start!
75
+ container.start
71
76
  output_container_logs(container)
72
77
  resp = container.wait
73
78
  output_container_logs(container)
@@ -97,10 +102,6 @@ module Wrapbox
97
102
  method_option :config_name, aliases: "-n", required: true, default: "default"
98
103
  method_option :environments, aliases: "-e"
99
104
  def run_cmd(*args)
100
- if args.size == 1
101
- args = args[0].split(" ")
102
- end
103
-
104
105
  repo = Wrapbox::ConfigRepository.new.tap { |r| r.load_yaml(options[:config]) }
105
106
  config = repo.get(options[:config_name])
106
107
  config.runner = :docker
@@ -108,7 +109,7 @@ module Wrapbox
108
109
  environments = options[:environments].to_s.split(/,\s*/).map { |kv| kv.split("=") }.map do |k, v|
109
110
  {name: k, value: v}
110
111
  end
111
- runner.run_cmd(*args, environments: environments)
112
+ runner.run_cmd(args, environments: environments)
112
113
  end
113
114
  end
114
115
  end
@@ -11,8 +11,15 @@ require "wrapbox/version"
11
11
  module Wrapbox
12
12
  module Runner
13
13
  class Ecs
14
- class ExecutionError < StandardError; end
14
+ class ExecutionFailure < StandardError; end
15
+ class ContainerAbnormalEnd < StandardError; end
16
+ class ExecutionTimeout < StandardError; end
15
17
  class LaunchFailure < StandardError; end
18
+ class LackResource < StandardError; end
19
+
20
+ EXECUTION_RETRY_INTERVAL = 3
21
+ WAIT_DELAY = 5
22
+ HOST_TERMINATED_REASON_REGEXP = /Host EC2.*terminated/
16
23
 
17
24
  attr_reader \
18
25
  :name,
@@ -31,125 +38,206 @@ module Wrapbox
31
38
  @container_definition = options[:container_definition]
32
39
  @additional_container_definitions = options[:additional_container_definitions]
33
40
  @task_role_arn = options[:task_role_arn]
41
+ $stdout.sync = true
34
42
  @logger = Logger.new($stdout)
35
43
  end
36
44
 
37
- def run(class_name, method_name, args, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
45
+ class Parameter
46
+ attr_reader \
47
+ :environments,
48
+ :task_role_arn,
49
+ :cluster,
50
+ :timeout,
51
+ :launch_timeout,
52
+ :launch_retry,
53
+ :retry_interval,
54
+ :retry_interval_multiplier,
55
+ :max_retry_interval,
56
+ :execution_retry
57
+
58
+ def initialize(environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120, execution_retry: 0)
59
+ b = binding
60
+ method(:initialize).parameters.each do |param|
61
+ instance_variable_set("@#{param[1]}", b.local_variable_get(param[1]))
62
+ end
63
+ end
64
+ end
65
+
66
+ def run(class_name, method_name, args, container_definition_overrides: {}, **parameters)
38
67
  task_definition = register_task_definition(container_definition_overrides)
68
+ parameter = Parameter.new(**parameters)
69
+
39
70
  run_task(
40
71
  task_definition.task_definition_arn, class_name, method_name, args,
41
- command: ["bundle", "exec", "rake", "wrapbox:run"],
42
- environments: environments,
43
- task_role_arn: task_role_arn || @task_role_arn,
44
- cluster: cluster,
45
- timeout: timeout,
46
- launch_timeout: launch_timeout,
47
- launch_retry: launch_retry,
48
- retry_interval: retry_interval,
49
- retry_interval_multiplier: retry_interval_multiplier,
50
- max_retry_interval: max_retry_interval,
72
+ ["bundle", "exec", "rake", "wrapbox:run"],
73
+ parameter
51
74
  )
52
75
  end
53
76
 
54
- def run_cmd(*cmd, container_definition_overrides: {}, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
77
+ def run_cmd(cmds, container_definition_overrides: {}, **parameters)
55
78
  task_definition = register_task_definition(container_definition_overrides)
79
+ parameter = Parameter.new(**parameters)
56
80
 
57
- run_task(
58
- task_definition.task_definition_arn, nil, nil, nil,
59
- command: cmd,
60
- environments: environments,
61
- task_role_arn: task_role_arn,
62
- cluster: cluster,
63
- timeout: timeout,
64
- launch_timeout: launch_timeout,
65
- launch_retry: launch_retry,
66
- retry_interval: retry_interval,
67
- retry_interval_multiplier: retry_interval_multiplier,
68
- max_retry_interval: max_retry_interval,
69
- )
81
+ ths = cmds.map do |cmd|
82
+ Thread.new(cmd) do |c|
83
+ run_task(
84
+ task_definition.task_definition_arn, nil, nil, nil,
85
+ c.split(/\s+/),
86
+ parameter
87
+ )
88
+ end
89
+ end
90
+ ths.each(&:join)
91
+ end
92
+
93
+ private
94
+
95
+ def run_task(task_definition_arn, class_name, method_name, args, command, parameter)
96
+ cl = parameter.cluster || self.cluster
97
+ execution_try_count = 0
98
+
99
+ begin
100
+ task = create_task(task_definition_arn, class_name, method_name, args, command, parameter)
101
+
102
+ @logger.debug("Launch Task: #{task.task_arn}")
103
+
104
+ wait_task_stopped(cl, task.task_arn, parameter.timeout)
105
+
106
+ @logger.debug("Stop Task: #{task.task_arn}")
107
+
108
+ # Avoid container exit code fetch miss
109
+ sleep WAIT_DELAY
110
+
111
+ task_status = fetch_task_status(cl, task.task_arn)
112
+
113
+ # If exit_code is nil, Container is force killed or ECS failed to launch Container by Irregular situation
114
+ error_message = "Container #{task_definition_name} is failed. task=#{task.task_arn}, exit_code=#{task_status[:exit_code]}, reason=#{task_status[:stopped_reason]}"
115
+ raise ContainerAbnormalEnd, error_message unless task_status[:exit_code]
116
+ raise ExecutionFailure, error_message unless task_status[:exit_code] == 0
117
+
118
+ true
119
+ rescue ContainerAbnormalEnd
120
+ retry if task_status[:stopped_reason] =~ HOST_TERMINATED_REASON_REGEXP
121
+
122
+ if execution_try_count >= parameter.execution_retry
123
+ raise
124
+ else
125
+ execution_try_count += 1
126
+ @logger.debug("Retry Execution after #{EXECUTION_RETRY_INTERVAL} sec")
127
+ sleep EXECUTION_RETRY_INTERVAL
128
+ retry
129
+ end
130
+ end
131
+ end
132
+
133
+ def task_definition_name
134
+ "wrapbox_#{name}"
70
135
  end
71
136
 
72
- def run_task(task_definition_arn, class_name, method_name, args, command:, environments: [], task_role_arn: nil, cluster: nil, timeout: 3600 * 24, launch_timeout: 60 * 10, launch_retry: 10, retry_interval: 1, retry_interval_multiplier: 2, max_retry_interval: 120)
73
- cl = cluster || self.cluster
137
+ def create_task(task_definition_arn, class_name, method_name, args, command, parameter)
138
+ cl = parameter.cluster || self.cluster
74
139
  args = Array(args)
75
140
 
76
141
  launch_try_count = 0
77
- current_retry_interval = retry_interval
78
- task = nil
79
- exit_code = nil
142
+ current_retry_interval = parameter.retry_interval
143
+
80
144
  begin
81
145
  task = client
82
- .run_task(build_run_task_options(class_name, method_name, args, command, environments, cluster, task_definition_arn, task_role_arn))
146
+ .run_task(build_run_task_options(task_definition_arn, class_name, method_name, args, command, cl, parameter.environments, parameter.task_role_arn))
83
147
  .tasks[0]
84
- raise LaunchFailure unless task
148
+
149
+ raise LackResource unless task # this case is almost lack of container resource.
150
+
85
151
  @logger.debug("Create Task: #{task.task_arn}")
86
- client.wait_until(:tasks_running, cluster: cl, tasks: [task.task_arn]) do |w|
87
- if launch_timeout
88
- w.delay = 5
89
- w.max_attempts = launch_timeout / w.delay
90
- else
91
- w.max_attempts = nil
92
- end
93
- end
94
- rescue Aws::Waiters::Errors::WaiterFailed, LaunchFailure
95
- exit_code = task && fetch_exit_code(cl, task.task_arn)
96
- unless exit_code
97
- if launch_try_count >= launch_retry
98
- client.stop_task(
99
- cluster: cl,
100
- task: task.task_arn,
101
- reason: "launch timeout"
102
- ) if task
103
- raise
104
- else
105
- put_waiting_task_count_metric(cl)
106
- launch_try_count += 1
107
- @logger.debug("Retry Create Task after #{current_retry_interval} sec")
108
- sleep current_retry_interval
109
- current_retry_interval = [current_retry_interval * retry_interval_multiplier, max_retry_interval].min
110
- retry
111
- end
112
- end
113
- end
114
152
 
115
- @logger.debug("Launch Task: #{task.task_arn}")
153
+ # Wait ECS Task Status becomes stable
154
+ sleep WAIT_DELAY
116
155
 
117
- begin
118
- client.wait_until(:tasks_stopped, cluster: cl, tasks: [task.task_arn]) do |w|
119
- if timeout
120
- w.delay = 5
121
- w.max_attempts = timeout / w.delay
156
+ begin
157
+ wait_task_running(cl, task.task_arn, parameter.launch_timeout)
158
+ task
159
+ rescue Aws::Waiters::Errors::TooManyAttemptsError
160
+ client.stop_task(
161
+ cluster: cl,
162
+ task: task.task_arn,
163
+ reason: "launch timeout"
164
+ )
165
+ raise
166
+ rescue Aws::Waiters::Errors::WaiterFailed
167
+ task_status = fetch_task_status(cl, task.task_arn)
168
+
169
+ case task_status[:last_status]
170
+ when "RUNNING"
171
+ return task
172
+ when "PENDING"
173
+ retry
122
174
  else
123
- w.max_attempts = nil
175
+ if task_status[:exit_code]
176
+ return task
177
+ else
178
+ raise LaunchFailure
179
+ end
124
180
  end
125
181
  end
126
- rescue Aws::Waiters::Errors::TooManyAttemptsError
127
- client.stop_task({
128
- cluster: cluster || self.cluster,
129
- task: task.task_arn,
130
- reason: "process timeout",
131
- })
132
- raise ExecutionError, "process timeout"
133
- end
182
+ rescue LackResource
183
+ @logger.debug("Failed to create task, because of lack resource")
184
+ put_waiting_task_count_metric(cl)
134
185
 
135
- @logger.debug("Stop Task: #{task.task_arn}")
136
-
137
- exit_code ||= fetch_exit_code(cl, task.task_arn)
138
- unless exit_code == 0
139
- raise ExecutionError, "Container #{task_definition_name} is failed. exit_code=#{exit_code}"
186
+ if launch_try_count >= parameter.launch_retry
187
+ raise
188
+ else
189
+ launch_try_count += 1
190
+ @logger.debug("Retry Create Task after #{current_retry_interval} sec")
191
+ sleep current_retry_interval
192
+ current_retry_interval = [current_retry_interval * parameter.retry_interval_multiplier, parameter.max_retry_interval].min
193
+ retry
194
+ end
195
+ rescue LaunchFailure
196
+ if launch_try_count >= parameter.launch_retry
197
+ raise
198
+ else
199
+ launch_try_count += 1
200
+ @logger.debug("Retry Create Task after #{current_retry_interval} sec")
201
+ sleep current_retry_interval
202
+ current_retry_interval = [current_retry_interval * parameter.retry_interval_multiplier, parameter.max_retry_interval].min
203
+ retry
204
+ end
140
205
  end
141
206
  end
142
207
 
143
- private
208
+ def wait_task_running(cluster, task_arn, launch_timeout)
209
+ client.wait_until(:tasks_running, cluster: cluster, tasks: [task_arn]) do |w|
210
+ if launch_timeout
211
+ w.delay = WAIT_DELAY
212
+ w.max_attempts = launch_timeout / w.delay
213
+ else
214
+ w.max_attempts = nil
215
+ end
216
+ end
217
+ end
144
218
 
145
- def task_definition_name
146
- "wrapbox_#{name}"
219
+ def wait_task_stopped(cluster, task_arn, timeout)
220
+ client.wait_until(:tasks_stopped, cluster: cluster, tasks: [task_arn]) do |w|
221
+ if timeout
222
+ w.delay = WAIT_DELAY
223
+ w.max_attempts = timeout / w.delay
224
+ else
225
+ w.max_attempts = nil
226
+ end
227
+ end
228
+ rescue Aws::Waiters::Errors::TooManyAttemptsError
229
+ client.stop_task({
230
+ cluster: cl,
231
+ task: task_arn,
232
+ reason: "process timeout",
233
+ })
234
+ raise ExecutionTimeout, "Container #{task_definition_name} is timeout. task=#{task_arn}, timeout=#{timeout}"
147
235
  end
148
236
 
149
- def fetch_exit_code(cluster, task_arn)
237
+ def fetch_task_status(cluster, task_arn)
150
238
  task = client.describe_tasks(cluster: cluster, tasks: [task_arn]).tasks[0]
151
239
  container = task.containers.find { |c| c.name = task_definition_name }
152
- container.exit_code
240
+ {last_status: task.last_status, exit_code: container.exit_code, stopped_reason: task.stopped_reason}
153
241
  end
154
242
 
155
243
  def register_task_definition(container_definition_overrides)
@@ -165,10 +253,18 @@ module Wrapbox
165
253
  end
166
254
  end
167
255
 
168
- client.register_task_definition({
169
- family: task_definition_name,
170
- container_definitions: container_definitions,
171
- }).task_definition
256
+ register_retry_count = 0
257
+ begin
258
+ client.register_task_definition({
259
+ family: task_definition_name,
260
+ container_definitions: container_definitions,
261
+ }).task_definition
262
+ rescue Aws::ECS::Errors::ClientException
263
+ raise if register_retry_count > 2
264
+ register_retry_count += 1
265
+ sleep 2
266
+ retry
267
+ end
172
268
  end
173
269
 
174
270
  def client
@@ -198,13 +294,14 @@ module Wrapbox
198
294
  value: cluster || self.cluster,
199
295
  },
200
296
  ],
297
+ timestamp: Time.now,
201
298
  value: 1.0,
202
299
  unit: "Count",
203
300
  ]
204
301
  )
205
302
  end
206
303
 
207
- def build_run_task_options(class_name, method_name, args, command, environments, cluster, task_definition_arn, task_role_arn)
304
+ def build_run_task_options(task_definition_arn, class_name, method_name, args, command, cluster, environments, task_role_arn)
208
305
  env = environments
209
306
  env += [
210
307
  {
@@ -252,12 +349,9 @@ module Wrapbox
252
349
  method_option :timeout, type: :numeric
253
350
  method_option :launch_timeout, type: :numeric
254
351
  method_option :launch_retry, type: :numeric
352
+ method_option :execution_retry, type: :numeric
255
353
  method_option :max_retry_interval, type: :numeric
256
354
  def run_cmd(*args)
257
- if args.size == 1
258
- args = args[0].split(" ")
259
- end
260
-
261
355
  repo = Wrapbox::ConfigRepository.new.tap { |r| r.load_yaml(options[:config]) }
262
356
  config = repo.get(options[:config_name])
263
357
  config.runner = :ecs
@@ -270,9 +364,10 @@ module Wrapbox
270
364
  timeout: options[:timeout],
271
365
  launch_timeout: options[:launch_timeout],
272
366
  launch_retry: options[:launch_retry],
367
+ execution_retry: options[:execution_retry],
273
368
  max_retry_interval: options[:max_retry_interval]
274
369
  }.reject { |_, v| v.nil? }
275
- runner.run_cmd(*args, environments: environments, **run_options)
370
+ runner.run_cmd(args, environments: environments, **run_options)
276
371
  end
277
372
  end
278
373
  end
@@ -1,3 +1,3 @@
1
1
  module Wrapbox
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wrapbox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - joker1007
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-02-17 00:00:00.000000000 Z
11
+ date: 2017-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk