heroku_hatchet 7.4.0 → 8.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +15 -22
- data/bin/hatchet +18 -7
- data/lib/hatchet/app.rb +28 -14
- data/lib/hatchet/reaper.rb +132 -115
- data/lib/hatchet/version.rb +1 -1
- data/lib/hatchet.rb +31 -8
- data/spec/hatchet/app_spec.rb +5 -38
- data/spec/hatchet/ci_spec.rb +0 -8
- data/spec/unit/default_ci_branch_spec.rb +27 -0
- data/spec/unit/reaper_spec.rb +14 -56
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f4233a137df7685f1e411471db8c53a72c9c6e77e5b9a901d0d8bb1c0147f06a
|
4
|
+
data.tar.gz: ffd93abf69b7a46ccbdda5148710e237b9c41171abef209ea56f187d7a52c501
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 61cc153424acbdf88c61b41ef9b4206fbb1e59c171ad30fded428d40bfe2edc728cdb2db72ca95526d1d5c5225d38eacacb6794b8c811b40988cac58480b0e74
|
7
|
+
data.tar.gz: 615e586767703298020a5d2a0172b9793a681a7cc681f45e6c6e42082e9db8c4a32f6e647fb006d8e9b7d8618b6c0dace7df6109cbca20e5e7fe0c34f991ed62
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,18 @@
|
|
1
1
|
## HEAD
|
2
2
|
|
3
|
+
## 8.0.1
|
4
|
+
|
5
|
+
- Bugfix: Lock and sleep and refresh API when duplicate app deletion detected (https://github.com/heroku/hatchet/pull/198)
|
6
|
+
|
7
|
+
## 8.0.0
|
8
|
+
|
9
|
+
- Breaking change: Delete apps on teardown. Previously hatchet would delete apps lazily to help with debugging. This behavior allowed developers to inspect logs and `heroku run bash` in the event of an unexpected failure. In practice, it is rarely needed and causes accounts to retain apps indefinitely. Previously there was no cost to retaining applications, but now `basic` applications incur a charge. Change details:
|
10
|
+
- The application teardown process now deletes applications directly.
|
11
|
+
- To skip destroying applications on teardown, set `HEROKU_DEBUG_EXPENSIVE=1`. This env var will cause `App#teardown!` to skip deletion so you can introspect why one failed.
|
12
|
+
- When hatchet needs a new application, it will first delete all applications that are created at least `HATCHET_ALIVE_TTL_MINUTES` ago. If it cannot delete any applications and the account is already at `HATCHET_APP_LIMIT`, it will sleep and try again later.
|
13
|
+
- Introduce `--older-than` flag to `hatchet destroy` CLI command. For example, the command `hatchet destroy --older-than=7`will remove any apps older than the provided value (in minutes).
|
14
|
+
- Add support for GitHub Actions env vars (https://github.com/heroku/hatchet/pull/189)
|
15
|
+
|
3
16
|
## 7.4.0
|
4
17
|
|
5
18
|
- Supports "basic" scaledown (https://github.com/heroku/hatchet/pull/193)
|
data/README.md
CHANGED
@@ -266,16 +266,17 @@ Hatchet::Runner.new("minimal_webpacker", buildpacks: buildpacks).deploy do |app,
|
|
266
266
|
expect($?.exitstatus).to eq(0)
|
267
267
|
expect($?.success?).to be_truthy
|
268
268
|
|
269
|
-
# In Ruby all objects except `nil` and `false` are "truthy"
|
270
|
-
#
|
269
|
+
# In Ruby all objects except `nil` and `false` are "truthy"
|
270
|
+
# in this case it could also be tested using `be_true` but
|
271
|
+
# it's best practice to use this `be_truthy` test helper instead
|
271
272
|
end
|
272
273
|
```
|
273
274
|
|
274
|
-
You can disable
|
275
|
+
You can disable error on exit status behavior, [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
|
275
276
|
|
276
277
|
- **Escaping and raw mode:**
|
277
278
|
|
278
|
-
By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb)
|
279
|
+
By default `app.run()` will escape the input so you can safely call `app.run("cmd && cmd")` and it works as expected. But if you want to do something custom, you can enable raw mode by passing in `raw: true` [see how to do it in the reference tests](https://github.com/heroku/hatchet/blob/master/spec/hatchet/app_spec.rb). If you do that, you'll need to ensure your inputs are propperly shell escaped.
|
279
280
|
|
280
281
|
- **Heroku options:**
|
281
282
|
|
@@ -358,21 +359,9 @@ And later:
|
|
358
359
|
Destroying "hatchet-t-fd25e3626b". Hatchet app limit: 80
|
359
360
|
```
|
360
361
|
|
361
|
-
|
362
|
+
If an app is not deleted for some reason, it will be deleted by a future test run. Applications not deleted by calling `teardown` will be allowed to live for `HATCHET_ALIVE_TTL_MINUTES`. This behavior allows multiple test runs on the same hatchet account without one test run deleting apps that another one still needs.
|
362
363
|
|
363
|
-
|
364
|
-
$ heroku run bash -a hatchet-t-bed73940a6
|
365
|
-
```
|
366
|
-
|
367
|
-
And use that to debug. Hatchet deletes old apps on demand. You tell it what your limits are and it will stay within those limits:
|
368
|
-
|
369
|
-
```
|
370
|
-
HATCHET_APP_LIMIT=20
|
371
|
-
```
|
372
|
-
|
373
|
-
With these env vars, Hatchet will "reap" older hatchet apps when it sees there are 20 or more hatchet apps. For CI, it's recommended that you increase the `HATCHET_APP_LIMIT` to 80-100. Hatchet will mark apps as safe for deletion once they've finished, and the `teardown!` method has been called on them (it tracks this by enabling maintenance mode on apps). Hatchet only tracks its apps. Hatchet uses a regex pattern on the name of apps to see which ones it can manage. If your account has reached the maximum number of global Heroku apps, you'll need to remove some manually.
|
374
|
-
|
375
|
-
If an app is not marked as being in maintenance mode for some reason, it can be deleted, but only after it has been allowed to live for some time. This behavior is configured by the `HATCHET_ALIVE_TTL_MINUTES` env var. For example, if you set it for `7`, Hatchet will ensure that any apps that are not marked as being in maintenance mode are allowed to live for at least seven minutes. This should give the app time to finish the test's execution, so it is not deleted mid-deploy. When this deletion happens, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. This deletion-mid-test behavior might otherwise be triggered if you have multiple CI runs executing at the same time.
|
364
|
+
For example, if you set `HATCHET_ALIVE_TTL_MINUTES=7`, Hatchet will ensure that any apps that are older than 7 minutes will be deleted. If all of your tests finish in under 7 minutes then apps won't be deleted mid-deploy. When apps are deleted, you'll see a warning in your output. It could indicate you're not properly cleaning up and calling `teardown!` on some of your apps, or it could mean that you're attempting to execute more tests concurrently than your `HATCHET_APP_LIMIT` allows. Or that something else prevented apps from getting deleted on teardown, such as a Heroku API outage.
|
376
365
|
|
377
366
|
It's recommended you don't use your personal Heroku API key for running tests on a CI server since the hatchet apps count against your account maximum limits. Running tests using your account locally is fine for debugging one or two tests.
|
378
367
|
|
@@ -684,7 +673,9 @@ end
|
|
684
673
|
- `app.platform_api`: Returns an instance of the [platform-api Heroku client](https://github.com/heroku/platform-api). If Hatchet doesn't give you access to a part of Heroku that you need, you can likely do it with the platform-api client.
|
685
674
|
- `app.push!`: Push code to your Heroku app. It can be used inside of a `deploy` block to re-deploy.
|
686
675
|
- `app.run_ci`: Runs Heroku CI against the app returns a TestRun object in the block
|
687
|
-
- `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will
|
676
|
+
- `app.teardown!`: This method is called automatically when using `app.deploy` in block mode after the deploy block finishes. When called it will delete the application.
|
677
|
+
|
678
|
+
Here is an example of a test that creates and deploys an app manually (without a block), then later tears it down manually. If you deploy an application without calling `teardown!` then hatchet will leak apps.
|
688
679
|
|
689
680
|
```ruby
|
690
681
|
before(:each) do
|
@@ -693,7 +684,7 @@ before(:each) do
|
|
693
684
|
end
|
694
685
|
|
695
686
|
after(:each) do
|
696
|
-
@app
|
687
|
+
@app&.teardown!
|
697
688
|
end
|
698
689
|
|
699
690
|
it "uses ruby" do
|
@@ -718,6 +709,7 @@ HATCHET_ALIVE_TTL_MINUTES=7
|
|
718
709
|
|
719
710
|
# HATCHET_RUN_MULTI=1 # WARNING: Setting this env var will incur charges against your account. To use this env var you must also enable `HATCHET_EXPENSIVE_MODE`
|
720
711
|
# HATCHET_EXPENSIVE_MODE=1 # WARNING: Do not set this environment variable unless you're okay with possibly large bills
|
712
|
+
# HEROKU_DEBUG_EXPENSIVE=1 # WARNING: This will prevent apps from being cleaned up automatically and you will be billed for any apps left running
|
721
713
|
```
|
722
714
|
|
723
715
|
> The syntax to set an env var in Ruby is `ENV["HATCHET_RETRIES"] = "2"` all env vars are strings.
|
@@ -725,12 +717,13 @@ HATCHET_ALIVE_TTL_MINUTES=7
|
|
725
717
|
- `HATCHET_BUILDPACK_BASE`: This is the URL where Hatchet can find your buildpack. It must be public for Heroku to be able to use your buildpack.
|
726
718
|
- `HATCHET_BUILDPACK_BRANCH`: By default, Hatchet will use your current git branch name. If, for some reason, git is not available or you want to manually specify it like `ENV["HATCHET_BUILDPACK_BRANCH'] = ENV[`MY_CI_BRANCH`]` then you can.
|
727
719
|
- `HATCHET_RETRIES` If the `ENV['HATCHET_RETRIES']` is set to a number, deploys are expected to work and automatically retry that number of times. Due to testing using a network and random failures, setting this value to `3` retries seems to work well. If an app cannot be deployed within its allotted number of retries, an error will be raised. The downside of a larger number is that your suite will keep running for much longer when there are legitimate failures.
|
728
|
-
- `HATCHET_APP_LIMIT`: The maximum number of
|
720
|
+
- `HATCHET_APP_LIMIT`: The maximum number of applications that hatchet is allowed to utilize on your account. If you hit this limit mid-test then Hatchet will need to wait until other tests finish and delete their apps. Keep in mind that you may have several concurrent CI executions on the same account competing for the same limited number of apps.
|
729
721
|
- `HEROKU_API_KEY`: The API key of your test account user. If you run locally without this set, it will use your personal credentials.
|
730
722
|
- `HEROKU_API_USER`: The email address of your user account. If you run locally without this set, it will use your personal credentials.
|
731
723
|
- `HATCHET_DEFAULT_STACK`: The default Heroku stack to be used when an explicit `stack` is not passed to `App.new`. If this is not set, apps will instead use Heroku platform's [default stack](https://devcenter.heroku.com/articles/stack#default-stack).
|
732
724
|
- `HATCHET_RUN_MULTI`: If enabled, this will scale up deployed apps to "standard-1x" once deployed instead of running on the free tier. This enables the `run_multi` method capability, however scaling up is not free. WARNING: Setting this env var will incur charges to your Heroku account. We recommended never to enable this setting unless you work for Heroku. To use this you must also set `HATCHET_EXPENSIVE_MODE=1`
|
733
725
|
- `HATCHET_EXPENSIVE_MODE`: This is intended to be a "safety" environment variable. If it is not set, Hatchet will prevent you from using the `run_multi: true` setting or the `HATCHET_RUN_MULTI` environment variables. There are still ways to incur charges without this feature, but unless you're absolutely confident your test setup will not leave "orphan" apps that are billing you, do not enable this setting. Even then, only set this value if you work for Heroku. To recap WARNING: setting this is expensive.
|
726
|
+
- `HEROKU_DEBUG_EXPENSIVE`: If set, hatchet will not delete applications on teardown. This can be used to introspect build logs or run `heroku run bash` on the application created for the failed test. Note that if another hatchet process runs after `HATCHET_ALIVE_TTL_MINUTES` then it will delete your applications.
|
734
727
|
|
735
728
|
## Basic
|
736
729
|
|
@@ -1037,7 +1030,7 @@ Ruby is full of multitudes, this isn't even close to being exhaustive, just enou
|
|
1037
1030
|
Hatchet has a CLI for installing and maintaining external repos you're
|
1038
1031
|
using to test against. If you have Hatchet installed as a gem run
|
1039
1032
|
|
1040
|
-
$
|
1033
|
+
$ hatchet --help
|
1041
1034
|
|
1042
1035
|
For more info on commands. If you're using the source code you can run
|
1043
1036
|
the command by going to the source code directory and running:
|
data/bin/hatchet
CHANGED
@@ -106,20 +106,31 @@ class HatchetCLI < Thor
|
|
106
106
|
end
|
107
107
|
end
|
108
108
|
|
109
|
-
desc "destroy
|
110
|
-
option :all, :
|
111
|
-
|
109
|
+
desc "destroy", "Deletes application(s)"
|
110
|
+
option :all, type: :boolean, desc: "Delete ALL hatchet apps"
|
111
|
+
option :older_than, type: :numeric, desc: "Delete all hatchet apps older than N minutes"
|
112
|
+
def destroy
|
112
113
|
api_key = ENV['HEROKU_API_KEY'] || `heroku auth:token`.chomp
|
113
114
|
platform_api = PlatformAPI.connect_oauth(api_key, cache: Moneta.new(:Null))
|
114
115
|
api_rate_limit = ApiRateLimit.new(platform_api)
|
115
116
|
reaper = Hatchet::Reaper.new(api_rate_limit: api_rate_limit)
|
116
117
|
|
117
|
-
|
118
|
+
case
|
119
|
+
when options[:all]
|
120
|
+
puts "Destroying ALL apps"
|
118
121
|
reaper.destroy_all
|
119
|
-
|
120
|
-
|
122
|
+
puts "Done"
|
123
|
+
when options[:older_than]
|
124
|
+
minutes = options[:older_than].to_i
|
125
|
+
puts "Destroying apps older than #{minutes}m"
|
126
|
+
reaper.destroy_older_apps(
|
127
|
+
minutes: minutes,
|
128
|
+
force_refresh: true,
|
129
|
+
on_conflict: :refresh_api_and_continue
|
130
|
+
)
|
131
|
+
puts "Done"
|
121
132
|
else
|
122
|
-
raise "
|
133
|
+
raise "No flags given run `hatchet help destroy` for options"
|
123
134
|
end
|
124
135
|
end
|
125
136
|
|
data/lib/hatchet/app.rb
CHANGED
@@ -11,7 +11,7 @@ module Hatchet
|
|
11
11
|
"https://github.com/heroku/heroku-buildpack-ruby.git"
|
12
12
|
}
|
13
13
|
}
|
14
|
-
HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] ||
|
14
|
+
HATCHET_BUILDPACK_BRANCH = -> { ENV['HATCHET_BUILDPACK_BRANCH'] || Hatchet.git_branch }
|
15
15
|
|
16
16
|
attr_reader :name, :stack, :repo_name, :app_config, :buildpacks, :reaper, :max_retries_count
|
17
17
|
|
@@ -67,6 +67,7 @@ module Hatchet
|
|
67
67
|
@repo_name = repo_name
|
68
68
|
@directory = self.config.path_for_name(@repo_name)
|
69
69
|
@name = name
|
70
|
+
@heroku_id = nil
|
70
71
|
@stack = stack
|
71
72
|
@debug = debug || debugging
|
72
73
|
@allow_failure = allow_failure
|
@@ -82,7 +83,6 @@ module Hatchet
|
|
82
83
|
end
|
83
84
|
@run_multi_array = []
|
84
85
|
@already_in_dir = nil
|
85
|
-
@app_is_setup = nil
|
86
86
|
|
87
87
|
@before_deploy_array = []
|
88
88
|
@before_deploy_array << before_deploy if before_deploy
|
@@ -280,11 +280,31 @@ module Hatchet
|
|
280
280
|
def create_app
|
281
281
|
3.times.retry do
|
282
282
|
begin
|
283
|
+
# Remove any obviously old apps first
|
284
|
+
# Try to use existing cache of apps to
|
285
|
+
# minimize API calls
|
286
|
+
@reaper.destroy_older_apps(
|
287
|
+
force_refresh: false,
|
288
|
+
on_conflict: :stop_if_under_limit,
|
289
|
+
)
|
283
290
|
hash = { name: name, stack: stack }
|
284
291
|
hash.delete_if { |k,v| v.nil? }
|
285
|
-
heroku_api_create_app(hash)
|
292
|
+
result = heroku_api_create_app(hash)
|
293
|
+
@heroku_id = result["id"]
|
286
294
|
rescue => e
|
287
|
-
|
295
|
+
# If we can't create an app assume
|
296
|
+
# it might be due to resource constraints
|
297
|
+
#
|
298
|
+
# Try to delete existing apps
|
299
|
+
@reaper.destroy_older_apps(
|
300
|
+
force_refresh: true,
|
301
|
+
on_conflict: :stop_if_under_limit,
|
302
|
+
)
|
303
|
+
# If we're still not under the limit, sleep a bit
|
304
|
+
# retry later.
|
305
|
+
@reaper.sleep_if_over_limit(
|
306
|
+
reason: "Could not create app #{e.message}"
|
307
|
+
)
|
288
308
|
raise e
|
289
309
|
end
|
290
310
|
end
|
@@ -301,7 +321,7 @@ module Hatchet
|
|
301
321
|
|
302
322
|
# creates a new heroku app via the API
|
303
323
|
def setup!
|
304
|
-
return self if @
|
324
|
+
return self if @heroku_id
|
305
325
|
puts "Hatchet setup: #{name.inspect} for #{repo_name.inspect}"
|
306
326
|
create_app
|
307
327
|
set_labs!
|
@@ -309,7 +329,6 @@ module Hatchet
|
|
309
329
|
api_rate_limit.call.buildpack_installation.update(name, updates: buildpack_list)
|
310
330
|
set_config @app_config
|
311
331
|
|
312
|
-
@app_is_setup = true
|
313
332
|
self
|
314
333
|
end
|
315
334
|
alias :setup :setup!
|
@@ -356,16 +375,11 @@ module Hatchet
|
|
356
375
|
end
|
357
376
|
|
358
377
|
def teardown!
|
359
|
-
return false unless @app_is_setup
|
360
|
-
|
361
|
-
if @run_multi_is_setup
|
362
378
|
@run_multi_array.map(&:join)
|
363
|
-
platform_api.formation.update(name, "web", {"size" => "basic"})
|
364
|
-
end
|
365
|
-
|
366
379
|
ensure
|
367
|
-
@
|
368
|
-
|
380
|
+
if @heroku_id && !ENV["HEROKU_DEBUG_EXPENSIVE"]
|
381
|
+
@reaper.destroy_with_log(name: @name, id: @heroku_id, reason: "teardown")
|
382
|
+
end
|
369
383
|
end
|
370
384
|
|
371
385
|
def in_directory
|
data/lib/hatchet/reaper.rb
CHANGED
@@ -1,138 +1,172 @@
|
|
1
1
|
require 'tmpdir'
|
2
2
|
|
3
3
|
module Hatchet
|
4
|
-
#
|
4
|
+
# Delete apps
|
5
5
|
#
|
6
|
-
#
|
7
|
-
# to delete an app to free up capacity. The goal of lazilly deleting apps is to temporarilly keep
|
8
|
-
# apps around for debugging if they fail.
|
6
|
+
# Delete a single app:
|
9
7
|
#
|
10
|
-
#
|
11
|
-
# on maintenance mode. The reaper will delete these in order (oldest first).
|
8
|
+
# @reaper.destroy_with_log(id: id, name: name, reason: "console")
|
12
9
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
10
|
+
# Clear out all apps older than HATCHET_ALIVE_TTL_MINUTES:
|
11
|
+
#
|
12
|
+
# @reaper.destroy_older_apps
|
13
|
+
#
|
14
|
+
# If you need to clear up space or wait for space to be cleared
|
15
|
+
# up then:
|
16
|
+
#
|
17
|
+
# @reaper.clean_old_or_sleep
|
17
18
|
#
|
18
|
-
# This class only limits and the number of "hatchet" apps on the system. Prevously there was a maximum of 100 apps on a
|
19
|
-
# Heroku account. Now a user can belong to multiple orgs and the total number of apps they have access to is no longer
|
20
|
-
# fixed at 100. Instead of hard coding a maximum limit, this failure mode is handled by forcing deletion of
|
21
|
-
# an app when app creation fails. In the future we may find a better way of detecting this failure mode
|
22
19
|
#
|
23
20
|
# Notes:
|
24
21
|
#
|
25
22
|
# - The class uses a file mutex so that multiple processes on the same machine do not attempt to run the
|
26
23
|
# reaper at the same time.
|
27
|
-
# - AlreadyDeletedError will be raised if an app has already been deleted (possibly by another test run on
|
28
|
-
# another machine). When this happens, the system will automatically attempt to reap another app.
|
29
24
|
class Reaper
|
30
25
|
class AlreadyDeletedError < StandardError; end
|
31
26
|
|
32
27
|
HATCHET_APP_LIMIT = Integer(ENV["HATCHET_APP_LIMIT"] || 20) # the number of apps hatchet keeps around
|
33
28
|
DEFAULT_REGEX = /^#{Regexp.escape(Hatchet::APP_PREFIX)}[a-f0-9]+/
|
29
|
+
TTL_MINUTES = ENV.fetch("HATCHET_ALIVE_TTL_MINUTES", "7").to_i
|
30
|
+
|
31
|
+
# Protect against parallel deletion on the same machine
|
32
|
+
# via concurrent processes
|
33
|
+
#
|
34
|
+
# Does not protect against distributed systems on different
|
35
|
+
# machines trying to delete the same applications
|
36
|
+
MUTEX_FILE = File.open(File.join(Dir.tmpdir(), "hatchet_reaper_mutex"), File::CREAT)
|
34
37
|
|
35
38
|
attr_accessor :io, :hatchet_app_limit
|
36
39
|
|
37
40
|
def initialize(api_rate_limit: , regex: DEFAULT_REGEX, io: STDOUT, hatchet_app_limit: HATCHET_APP_LIMIT, initial_sleep: 10)
|
38
|
-
@api_rate_limit = api_rate_limit
|
39
|
-
@regex = regex
|
40
41
|
@io = io
|
41
|
-
@
|
42
|
-
@
|
43
|
-
@
|
44
|
-
@
|
42
|
+
@apps = []
|
43
|
+
@regex = regex
|
44
|
+
@limit = hatchet_app_limit
|
45
|
+
@api_rate_limit = api_rate_limit
|
45
46
|
@reaper_throttle = ReaperThrottle.new(initial_sleep: initial_sleep)
|
46
47
|
end
|
47
48
|
|
48
|
-
def
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
refresh_app_list if @finished_hatchet_apps.empty?
|
54
|
-
|
55
|
-
# To be safe try to delete an app even if we're not over the limit
|
56
|
-
# since the exception may have been caused by going over the maximum account limit
|
57
|
-
if app_exception_message
|
49
|
+
def sleep_if_over_limit(reason: )
|
50
|
+
if @apps.length >= @limit
|
51
|
+
age = AppAge.new(created_at: @apps.last["created_at"], ttl_minutes: TTL_MINUTES)
|
52
|
+
@reaper_throttle.call(max_sleep: age.sleep_for_ttl) do |sleep_for|
|
58
53
|
io.puts <<-EOM.strip_heredoc
|
59
|
-
WARNING:
|
60
|
-
|
61
|
-
|
54
|
+
WARNING: Hatchet app limit reached (#{@apps.length}/#{@limit})
|
55
|
+
All known apps are younger than #{TTL_MINUTES} minutes.
|
56
|
+
Sleeping (#{sleep_for}s)
|
57
|
+
|
58
|
+
Reason: #{reason}
|
62
59
|
EOM
|
63
|
-
reap_once
|
64
|
-
end
|
65
60
|
|
66
|
-
|
67
|
-
|
61
|
+
sleep(sleep_for)
|
62
|
+
end
|
68
63
|
end
|
69
|
-
ensure
|
70
|
-
mutex_file.close
|
71
64
|
end
|
72
65
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
66
|
+
# Destroys apps that are older than the given argument (expecting integer minutes)
|
67
|
+
#
|
68
|
+
# This method might be running concurrently on multiple processes or multiple
|
69
|
+
# machines.
|
70
|
+
#
|
71
|
+
# When a duplicate destroy is detected we can move forward with a conflict strategy:
|
72
|
+
#
|
73
|
+
# - `:refresh_api_and_continue`: Sleep to see if another process will clean up everything for
|
74
|
+
# us and then re-populate apps from the API and continue.
|
75
|
+
# - `:stop_if_under_limit`: Sleep to allow other processes to continue. Then if apps list
|
76
|
+
# is under the limit, assume someone else is already cleaning up for us and that we're
|
77
|
+
# good to move ahead to try to create an app. Otherwise if we're at or
|
78
|
+
# over the limit sleep, refresh the app list, and continue attempting to delete apps.
|
79
|
+
def destroy_older_apps(minutes: TTL_MINUTES, force_refresh: @apps.empty?, on_conflict: :refresh_api_and_continue)
|
80
|
+
MUTEX_FILE.flock(File::LOCK_EX)
|
81
|
+
|
82
|
+
refresh_app_list if force_refresh
|
83
|
+
|
84
|
+
while app = @apps.pop
|
85
|
+
age = AppAge.new(created_at: app["created_at"], ttl_minutes: minutes)
|
86
|
+
if !age.can_delete?
|
87
|
+
@apps.push(app)
|
88
|
+
break
|
89
|
+
else
|
90
|
+
begin
|
91
|
+
destroy_with_log(
|
92
|
+
id: app["id"],
|
93
|
+
name: app["name"],
|
94
|
+
reason: "app age (#{age.in_minutes}m) is older than #{minutes}m"
|
95
|
+
)
|
96
|
+
rescue AlreadyDeletedError => e
|
97
|
+
if handle_conflict(
|
98
|
+
strategy: on_conflict,
|
99
|
+
conflict_message: e.message,
|
100
|
+
) == :stop
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
ensure
|
107
|
+
MUTEX_FILE.flock(File::LOCK_UN)
|
79
108
|
end
|
80
109
|
|
81
110
|
# No guardrails, will delete all apps that match the hatchet namespace
|
82
|
-
def destroy_all
|
83
|
-
|
111
|
+
def destroy_all(force_refresh: @apps.empty?)
|
112
|
+
MUTEX_FILE.flock(File::LOCK_EX)
|
84
113
|
|
85
|
-
|
114
|
+
refresh_app_list if force_refresh
|
115
|
+
|
116
|
+
while app = @apps.pop
|
86
117
|
begin
|
87
|
-
destroy_with_log(name: app["name"], id: app["id"])
|
88
|
-
rescue AlreadyDeletedError
|
89
|
-
|
118
|
+
destroy_with_log(name: app["name"], id: app["id"], reason: "destroy all")
|
119
|
+
rescue AlreadyDeletedError => e
|
120
|
+
handle_conflict(
|
121
|
+
conflict_message: e.message,
|
122
|
+
strategy: :refresh_api_and_continue
|
123
|
+
)
|
90
124
|
end
|
91
125
|
end
|
126
|
+
ensure
|
127
|
+
MUTEX_FILE.flock(File::LOCK_UN)
|
92
128
|
end
|
93
129
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
130
|
+
# Will sleep with backoff and emit a warning message
|
131
|
+
# returns :continue or :stop symbols
|
132
|
+
# :stop indicates execution should stop
|
133
|
+
private def handle_conflict(conflict_message:, strategy:)
|
134
|
+
message = String.new(<<-EOM.strip_heredoc)
|
135
|
+
WARNING: Possible race condition detected: #{conflict_message}
|
136
|
+
Hatchet app limit (#{@apps.length}/#{@limit}), using strategy #{strategy}
|
137
|
+
EOM
|
138
|
+
|
139
|
+
conflict_state = if :refresh_api_and_continue == strategy
|
140
|
+
message << "\nSleeping, refreshing app list, and continuing."
|
141
|
+
:continue
|
142
|
+
elsif :stop_if_under_limit == strategy && @apps.length >= @limit
|
143
|
+
message << "\nSleeping, refreshing app list, and continuing. Not under limit."
|
144
|
+
:continue
|
145
|
+
elsif :stop_if_under_limit == strategy
|
146
|
+
message << "\nHalting deletion of older apps. Under limit."
|
147
|
+
:stop
|
148
|
+
else
|
149
|
+
raise "No such strategy: #{strategy}, plese use :stop_if_under_limit or :refresh_api_and_continue"
|
101
150
|
end
|
102
|
-
rescue AlreadyDeletedError
|
103
|
-
retry
|
104
|
-
end
|
105
151
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
age = AppAge.new(
|
112
|
-
created_at: app["created_at"],
|
113
|
-
ttl_minutes: ENV.fetch("HATCHET_ALIVE_TTL_MINUTES", "7").to_i
|
114
|
-
)
|
115
|
-
if age.can_delete?
|
116
|
-
io.puts "WARNING: Destroying an app without maintenance mode on, app: #{app["name"]}, app_age: #{age.in_minutes} minutes"
|
117
|
-
|
118
|
-
destroy_with_log(name: app["name"], id: app["id"])
|
119
|
-
else
|
120
|
-
# We're not going to delete it yet, so put it back
|
121
|
-
@unfinished_hatchet_apps << app
|
152
|
+
@reaper_throttle.call(max_sleep: TTL_MINUTES) do |sleep_for|
|
153
|
+
io.puts <<-EOM.strip_heredoc
|
154
|
+
#{message}
|
155
|
+
Sleeping (#{sleep_for}s)
|
156
|
+
EOM
|
122
157
|
|
123
|
-
|
124
|
-
|
125
|
-
io.puts <<-EOM.strip_heredoc
|
126
|
-
WARNING: Attempting to destroy an app without maintenance mode on, but it is not old enough. app: #{app["name"]}, app_age: #{age.in_minutes} minutes
|
127
|
-
This can happen if App#teardown! is not called on an application, which will leave it in an 'unfinished' state
|
128
|
-
This can also happen if you're trying to run more tests concurrently than your currently set value for HATCHET_APP_COUNT
|
129
|
-
Sleeping: #{sleep_for} seconds before trying to find another app to reap"
|
130
|
-
#{stats_string}, HATCHET_ALIVE_TTL_MINUTES=#{age.ttl_minutes}
|
131
|
-
EOM
|
158
|
+
sleep(sleep_for)
|
159
|
+
end
|
132
160
|
|
133
|
-
|
134
|
-
|
161
|
+
case conflict_state
|
162
|
+
when :continue
|
163
|
+
refresh_app_list
|
164
|
+
when :stop
|
165
|
+
else
|
166
|
+
raise "Unknown state #{conflict_state}"
|
135
167
|
end
|
168
|
+
|
169
|
+
conflict_state
|
136
170
|
end
|
137
171
|
|
138
172
|
private def get_heroku_apps
|
@@ -140,28 +174,15 @@ module Hatchet
|
|
140
174
|
end
|
141
175
|
|
142
176
|
private def refresh_app_list
|
143
|
-
apps = get_heroku_apps.
|
177
|
+
@apps = get_heroku_apps.
|
178
|
+
filter {|app| app["name"].match(@regex) }.
|
144
179
|
map {|app| app["created_at"] = DateTime.parse(app["created_at"].to_s); app }.
|
145
180
|
sort_by { |app| app["created_at"] }.
|
146
181
|
reverse # Ascending order, oldest is last
|
147
|
-
|
148
|
-
@app_count = apps.length
|
149
|
-
|
150
|
-
@finished_hatchet_apps.clear
|
151
|
-
@unfinished_hatchet_apps.clear
|
152
|
-
apps.each do |app|
|
153
|
-
next unless app["name"].match(@regex)
|
154
|
-
|
155
|
-
if app["maintenance"]
|
156
|
-
@finished_hatchet_apps << app
|
157
|
-
else
|
158
|
-
@unfinished_hatchet_apps << app
|
159
|
-
end
|
160
|
-
end
|
161
182
|
end
|
162
183
|
|
163
|
-
|
164
|
-
message = "Destroying #{name.inspect}: #{id}, #{
|
184
|
+
def destroy_with_log(name:, id:, reason: )
|
185
|
+
message = "Destroying #{name.inspect}: #{id}, (#{@apps.length}/#{@limit}) reason: #{reason}"
|
165
186
|
|
166
187
|
@api_rate_limit.call.app.delete(id)
|
167
188
|
|
@@ -170,19 +191,15 @@ module Hatchet
|
|
170
191
|
body = e.response.body
|
171
192
|
request_id = e.response.headers["Request-Id"]
|
172
193
|
if body =~ /Couldn\'t find that app./
|
173
|
-
|
174
|
-
raise AlreadyDeletedError.new
|
194
|
+
message = "Duplicate destroy attempted #{name.inspect}: #{id}, status: 404, request_id: #{request_id}"
|
195
|
+
raise AlreadyDeletedError.new(message)
|
175
196
|
else
|
176
197
|
raise e
|
177
198
|
end
|
178
199
|
rescue Excon::Error::Forbidden => e
|
179
200
|
request_id = e.response.headers["Request-Id"]
|
180
|
-
|
181
|
-
raise AlreadyDeletedError.new
|
182
|
-
end
|
183
|
-
|
184
|
-
private def hatchet_app_count
|
185
|
-
@finished_hatchet_apps.length + @unfinished_hatchet_apps.length
|
201
|
+
message = "Duplicate destroy attempted #{name.inspect}: #{id}, status: 403, request_id: #{request_id}"
|
202
|
+
raise AlreadyDeletedError.new(message)
|
186
203
|
end
|
187
204
|
end
|
188
205
|
end
|
data/lib/hatchet/version.rb
CHANGED
data/lib/hatchet.rb
CHANGED
@@ -21,20 +21,43 @@ require 'hatchet/api_rate_limit'
|
|
21
21
|
require 'hatchet/init_project'
|
22
22
|
require 'hatchet/heroku_run'
|
23
23
|
|
24
|
+
class DefaultCIBranch
|
25
|
+
def initialize(env: ENV)
|
26
|
+
@env = env
|
27
|
+
end
|
28
|
+
|
29
|
+
def call
|
30
|
+
# https://circleci.com/docs/variables
|
31
|
+
return @env['CIRCLE_BRANCH'] if @env['CIRCLE_BRANCH']
|
32
|
+
# https://docs.github.com/en/actions/learn-github-actions/environment-variables
|
33
|
+
# GITHUB_HEAD_REF is provided for PRs, but blank for branch actions.
|
34
|
+
return @env['GITHUB_HEAD_REF'] if @env['GITHUB_HEAD_REF'] && !@env['GITHUB_HEAD_REF']&.empty?
|
35
|
+
# GITHUB_REF_NAME is incorrect on PRs (`1371/merge`), but correct for branch actions.
|
36
|
+
return @env['GITHUB_REF_NAME'] if @env['GITHUB_REF_NAME']
|
37
|
+
# https://devcenter.heroku.com/articles/heroku-ci#immutable-environment-variables
|
38
|
+
return @env['HEROKU_TEST_RUN_BRANCH'] if @env['HEROKU_TEST_RUN_BRANCH']
|
39
|
+
# TRAVIS_BRANCH works fine unless the build is a pull-request. In that case, it will contain the target branch
|
40
|
+
# not the actual pull-request branch! TRAVIS_PULL_REQUEST_BRANCH contains the correct branch but will be empty
|
41
|
+
# for push builds. See: https://docs.travis-ci.com/user/environment-variables/
|
42
|
+
return @env['TRAVIS_PULL_REQUEST_BRANCH'] if @env['TRAVIS_PULL_REQUEST_BRANCH'] && !@env['TRAVIS_PULL_REQUEST_BRANCH']&.empty?
|
43
|
+
return @env['TRAVIS_BRANCH'] if @env['TRAVIS_BRANCH']
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
24
47
|
module Hatchet
|
25
48
|
RETRIES = Integer(ENV['HATCHET_RETRIES'] || 1)
|
26
49
|
Runner = Hatchet::GitApp
|
27
50
|
|
28
51
|
def self.git_branch
|
29
|
-
|
30
|
-
# not the actual pull-request branch! TRAVIS_PULL_REQUEST_BRANCH contains the correct branch but will be empty
|
31
|
-
# for push builds. See: https://docs.travis-ci.com/user/environment-variables/
|
32
|
-
return ENV['TRAVIS_PULL_REQUEST_BRANCH'] if ENV['TRAVIS_PULL_REQUEST_BRANCH'] && !ENV['TRAVIS_PULL_REQUEST_BRANCH'].empty?
|
33
|
-
return ENV['TRAVIS_BRANCH'] if ENV['TRAVIS_BRANCH']
|
52
|
+
branch = DefaultCIBranch.new.call
|
34
53
|
|
35
|
-
|
36
|
-
|
37
|
-
|
54
|
+
if branch
|
55
|
+
branch
|
56
|
+
else
|
57
|
+
out = `git rev-parse --abbrev-ref HEAD`.strip
|
58
|
+
raise "Attempting to find current branch name. Error: Cannot describe git: #{out}" unless $?.success?
|
59
|
+
out
|
60
|
+
end
|
38
61
|
end
|
39
62
|
|
40
63
|
if ENV["HATCHET_DEBUG_DEADLOCK"]
|
data/spec/hatchet/app_spec.rb
CHANGED
@@ -43,14 +43,14 @@ describe "AppTest" do
|
|
43
43
|
|
44
44
|
reaper = app.reaper
|
45
45
|
|
46
|
-
def reaper.
|
47
|
-
def reaper.
|
46
|
+
def reaper.destroy_older_apps(*args, **kwargs, &block); @app_exception_message = true; end
|
47
|
+
def reaper.clean_old_was_called?; @app_exception_message; end
|
48
48
|
|
49
49
|
expect {
|
50
50
|
app.create_app
|
51
51
|
}.to raise_error("made you look")
|
52
52
|
|
53
|
-
expect(reaper.
|
53
|
+
expect(reaper.clean_old_was_called?).to be_truthy
|
54
54
|
end
|
55
55
|
|
56
56
|
it "app with default" do
|
@@ -82,37 +82,6 @@ describe "AppTest" do
|
|
82
82
|
end
|
83
83
|
end
|
84
84
|
|
85
|
-
it "marks itself 'finished' when done in block mode" do
|
86
|
-
app = Hatchet::Runner.new("default_ruby")
|
87
|
-
|
88
|
-
def app.push_with_retry!; nil; end
|
89
|
-
app.deploy do |app|
|
90
|
-
expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
|
91
|
-
end
|
92
|
-
|
93
|
-
# After the app is updated, there's no guarantee it will still exist
|
94
|
-
# so we cannot rely on an api call to determine maintenance mode
|
95
|
-
app_update_info = app.instance_variable_get(:"@app_update_info")
|
96
|
-
expect(app_update_info["name"]).to eq(app.name)
|
97
|
-
expect(app_update_info["maintenance"]).to be_truthy
|
98
|
-
end
|
99
|
-
|
100
|
-
it "marks itself 'finished' when done in non-block mode" do
|
101
|
-
app = Hatchet::Runner.new("default_ruby")
|
102
|
-
|
103
|
-
def app.push_with_retry!; nil; end
|
104
|
-
app.deploy
|
105
|
-
expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
|
106
|
-
|
107
|
-
app.teardown!
|
108
|
-
|
109
|
-
# After the app is updated, there's no guarantee it will still exist
|
110
|
-
# so we cannot rely on an api call to determine maintenance mode
|
111
|
-
app_update_info = app.instance_variable_get(:"@app_update_info")
|
112
|
-
expect(app_update_info["name"]).to eq(app.name)
|
113
|
-
expect(app_update_info["maintenance"]).to be_truthy
|
114
|
-
end
|
115
|
-
|
116
85
|
describe "before deploy" do
|
117
86
|
it "dir" do
|
118
87
|
@called = false
|
@@ -263,16 +232,14 @@ describe "AppTest" do
|
|
263
232
|
@run_count = AtomicCount.new(0)
|
264
233
|
app = Hatchet::GitApp.new("default_ruby", run_multi: true)
|
265
234
|
app.deploy do
|
266
|
-
app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1) }
|
267
|
-
app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1) }
|
235
|
+
app.run_multi("ls") { |out| expect(out).to include("Gemfile"); @run_count.add(1); }
|
236
|
+
app.run_multi("blerg -v") { |_, status| expect(status.success?).to be_falsey; @run_count.add(1); }
|
268
237
|
app.run_multi("ruby -v") do |out, status|
|
269
238
|
expect(out).to include("ruby")
|
270
239
|
expect(status.success?).to be_truthy
|
271
240
|
|
272
241
|
@run_count.add(1)
|
273
242
|
end
|
274
|
-
|
275
|
-
expect(app.platform_api.formation.list(app.name).detect {|ps| ps["type"] == "web"}["size"].downcase).to_not eq("free")
|
276
243
|
end
|
277
244
|
|
278
245
|
# After the deploy block exits `teardown!` is called
|
data/spec/hatchet/ci_spec.rb
CHANGED
@@ -15,15 +15,7 @@ describe "CIFourTest" do
|
|
15
15
|
expect(test_run.output).to match(string)
|
16
16
|
expect(test_run.output).to match("Using rake")
|
17
17
|
expect(test_run.output).to_not match("Installing rake")
|
18
|
-
|
19
|
-
expect(app.platform_api.app.info(app.name)["maintenance"]).to be_falsey
|
20
18
|
end
|
21
|
-
|
22
|
-
# After the app is updated, there's no guarantee it will still exist
|
23
|
-
# so we cannot rely on an api call to determine maintenance mode
|
24
|
-
app_update_info = app.instance_variable_get(:"@app_update_info")
|
25
|
-
expect(app_update_info["name"]).to eq(app.name)
|
26
|
-
expect(app_update_info["maintenance"]).to be_truthy
|
27
19
|
end
|
28
20
|
|
29
21
|
it "error with bad app" do
|
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe "DefaultCIBranch" do
|
5
|
+
it "doesn't error on empty env" do
|
6
|
+
out = DefaultCIBranch.new(env: {}).call
|
7
|
+
expect(out).to be_nil
|
8
|
+
end
|
9
|
+
|
10
|
+
it "GitHub PRs" do
|
11
|
+
out = DefaultCIBranch.new(env: {"GITHUB_HEAD_REF" => "iAmaPR"}).call
|
12
|
+
expect(out).to eq("iAmaPR")
|
13
|
+
|
14
|
+
out = DefaultCIBranch.new(env: {"GITHUB_HEAD_REF" => ""}).call
|
15
|
+
expect(out).to be_nil
|
16
|
+
end
|
17
|
+
|
18
|
+
it "GitHub branches" do
|
19
|
+
out = DefaultCIBranch.new(env: {"GITHUB_REF_NAME" => "iAmaBranch"}).call
|
20
|
+
expect(out).to eq("iAmaBranch")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "heroku" do
|
24
|
+
out = DefaultCIBranch.new(env: {"HEROKU_TEST_RUN_BRANCH" => "iAmaBranch"}).call
|
25
|
+
expect(out).to eq("iAmaBranch")
|
26
|
+
end
|
27
|
+
end
|
data/spec/unit/reaper_spec.rb
CHANGED
@@ -18,7 +18,7 @@ describe "Reaper" do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
describe "cycle" do
|
21
|
-
it "does not delete anything if
|
21
|
+
it "does not delete anything if no old apps" do
|
22
22
|
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
|
23
23
|
|
24
24
|
def reaper.get_heroku_apps
|
@@ -29,46 +29,11 @@ describe "Reaper" do
|
|
29
29
|
def reaper.check_get_heroku_apps_called; @called_get_heroku_apps ; end
|
30
30
|
def reaper.reap_once; raise "should not be called"; end
|
31
31
|
|
32
|
-
reaper.
|
32
|
+
reaper.destroy_older_apps
|
33
33
|
|
34
34
|
expect(reaper.check_get_heroku_apps_called).to be_truthy
|
35
35
|
end
|
36
36
|
|
37
|
-
it "deletes a maintenance mode app on error" do
|
38
|
-
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 1, io: StringIO.new)
|
39
|
-
|
40
|
-
def reaper.get_heroku_apps
|
41
|
-
@mock_apps ||= [
|
42
|
-
{"name" => "hatchet-t-unfinished", "id" => 2, "maintenance" => false, "created_at" => Time.now.to_s},
|
43
|
-
{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}
|
44
|
-
]
|
45
|
-
end
|
46
|
-
def reaper.destroy_with_log(name: , id: )
|
47
|
-
@reaper_destroy_called_with = {"name" => name, "id" => id}
|
48
|
-
end
|
49
|
-
def reaper.destroy_called_with; @reaper_destroy_called_with; end
|
50
|
-
|
51
|
-
reaper.cycle(app_exception_message: true)
|
52
|
-
|
53
|
-
expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
|
54
|
-
end
|
55
|
-
|
56
|
-
it "deletes maintenance mode app when over limit" do
|
57
|
-
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
|
58
|
-
|
59
|
-
def reaper.get_heroku_apps
|
60
|
-
@mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => true, "created_at" => Time.now.to_s}]
|
61
|
-
end
|
62
|
-
def reaper.destroy_with_log(name: , id: )
|
63
|
-
@reaper_destroy_called_with = {"name" => name, "id" => id}
|
64
|
-
end
|
65
|
-
def reaper.destroy_called_with; @reaper_destroy_called_with; end
|
66
|
-
|
67
|
-
reaper.cycle
|
68
|
-
|
69
|
-
expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
|
70
|
-
end
|
71
|
-
|
72
37
|
it "deletes an old app that is past TLL" do
|
73
38
|
reaper = Hatchet::Reaper.new(api_rate_limit: Object.new, hatchet_app_limit: 0, io: StringIO.new)
|
74
39
|
|
@@ -76,25 +41,30 @@ describe "Reaper" do
|
|
76
41
|
two_days_ago = DateTime.now.new_offset(0) - 2
|
77
42
|
@mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => two_days_ago.to_s }]
|
78
43
|
end
|
79
|
-
def reaper.destroy_with_log(name: , id: )
|
44
|
+
def reaper.destroy_with_log(name: , id: , reason: )
|
80
45
|
@reaper_destroy_called_with = {"name" => name, "id" => id}
|
81
46
|
end
|
82
47
|
def reaper.destroy_called_with; @reaper_destroy_called_with; end
|
83
48
|
|
84
|
-
reaper.
|
49
|
+
reaper.destroy_older_apps
|
85
50
|
|
86
51
|
expect(reaper.destroy_called_with).to eq({"name" => "hatchet-t-foo", "id" => 1})
|
87
52
|
end
|
88
53
|
|
89
54
|
it "sleeps, refreshes app list, and tries again when an old app is not past TTL" do
|
90
55
|
warning = StringIO.new
|
91
|
-
reaper = Hatchet::Reaper.new(
|
56
|
+
reaper = Hatchet::Reaper.new(
|
57
|
+
io: warning,
|
58
|
+
initial_sleep: 0,
|
59
|
+
api_rate_limit: Object.new,
|
60
|
+
hatchet_app_limit: 0,
|
61
|
+
)
|
92
62
|
|
93
63
|
def reaper.get_heroku_apps
|
94
64
|
now = DateTime.now.new_offset(0)
|
95
65
|
@mock_apps ||= [{"name" => "hatchet-t-foo", "id" => 1, "maintenance" => false, "created_at" => now.to_s }]
|
96
66
|
end
|
97
|
-
def reaper.destroy_with_log(name: , id: )
|
67
|
+
def reaper.destroy_with_log(name: , id: , reason: )
|
98
68
|
@reaper_destroy_called_with = {"name" => name, "id" => id}
|
99
69
|
end
|
100
70
|
def reaper.destroy_called_with; @reaper_destroy_called_with; end
|
@@ -104,13 +74,13 @@ describe "Reaper" do
|
|
104
74
|
|
105
75
|
def reaper.get_slept_for_val; @_slept_for; end
|
106
76
|
|
107
|
-
reaper.
|
77
|
+
reaper.destroy_older_apps
|
78
|
+
reaper.sleep_if_over_limit(reason: "test")
|
108
79
|
|
109
80
|
expect(reaper.get_slept_for_val).to eq(0)
|
110
81
|
expect(reaper.destroy_called_with).to eq(nil)
|
111
82
|
|
112
|
-
expect(warning.string).to
|
113
|
-
expect(warning.string).to match("total_app_count: 1, hatchet_app_count: 1/#{Hatchet::Reaper::HATCHET_APP_LIMIT}, finished: 0, unfinished: 1")
|
83
|
+
expect(warning.string).to include("WARNING: Hatchet app limit reached (1/0)")
|
114
84
|
end
|
115
85
|
end
|
116
86
|
|
@@ -154,16 +124,4 @@ describe "Reaper" do
|
|
154
124
|
end
|
155
125
|
end
|
156
126
|
end
|
157
|
-
|
158
|
-
it "over limit" do
|
159
|
-
reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
|
160
|
-
def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT + 1; end
|
161
|
-
|
162
|
-
expect(reaper.over_limit?).to be_truthy
|
163
|
-
|
164
|
-
reaper = Hatchet::Reaper.new(api_rate_limit: -> (){}, io: StringIO.new)
|
165
|
-
def reaper.hatchet_app_count; Hatchet::Reaper::HATCHET_APP_LIMIT - 1; end
|
166
|
-
|
167
|
-
expect(reaper.over_limit?).to be_falsey
|
168
|
-
end
|
169
127
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: heroku_hatchet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 8.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Richard Schneeman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-01
|
11
|
+
date: 2023-03-01 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: platform-api
|
@@ -206,6 +206,7 @@ files:
|
|
206
206
|
- spec/hatchet/local_repo_spec.rb
|
207
207
|
- spec/hatchet/lock_spec.rb
|
208
208
|
- spec/spec_helper.rb
|
209
|
+
- spec/unit/default_ci_branch_spec.rb
|
209
210
|
- spec/unit/heroku_run_spec.rb
|
210
211
|
- spec/unit/init_spec.rb
|
211
212
|
- spec/unit/reaper_spec.rb
|
@@ -245,6 +246,7 @@ test_files:
|
|
245
246
|
- spec/hatchet/local_repo_spec.rb
|
246
247
|
- spec/hatchet/lock_spec.rb
|
247
248
|
- spec/spec_helper.rb
|
249
|
+
- spec/unit/default_ci_branch_spec.rb
|
248
250
|
- spec/unit/heroku_run_spec.rb
|
249
251
|
- spec/unit/init_spec.rb
|
250
252
|
- spec/unit/reaper_spec.rb
|