deployangel 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 59eed3c6629b0dc5b81cbec2636583d50f0038c2f51af8011108205a1a23eb8c
4
- data.tar.gz: d10e3a967bed17b69b854a5b74deba72b66ad4e204b3c007bd1958fc81e1ca66
3
+ metadata.gz: 2a115a5a35d86c5edce65dad1cdf3947833416ea66f47c69da18b85de3047153
4
+ data.tar.gz: ec954e62605c6452d3e374896085343cb6a12910e0b821794342a2e0a6dc0426
5
5
  SHA512:
6
- metadata.gz: cecef6e5f9328a9b72877c16bbd51388d67a5673b98a75a56c78e6916f5bf99fd033baaf9056f101b25cd36553d0e6033a253800f571ecfab3724655f1301901
7
- data.tar.gz: cb8460812b9f04eaf761e23282a644c62f4de9aaca20fcf42e1bc81478f898e096675fd1bdfc67e70aef60ac3053bce969de7310539b9261b036c262dae70bd1
6
+ metadata.gz: 4c18ca61ecff8936bdfa7f4a5900fd27b96c2a787094e504593fae78f62eaee3c7d4ea11e74811b619313c1a726bce8e4644f5a6319c687450ff81629acdd2a5
7
+ data.tar.gz: 2754b3cc315c626df0dc57de4723721caa668f69b7b523a0ca4eaecaf4f84e2f2ec7f9f40f366be34f9a1989ff3c9b4650f1d02dc81b976218d2b3daf5e3968c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.21 (2026-10-09)
4
+
5
+ - `deployangel exercise` stops requesting a page after its first 404, 405,
6
+ or 410, since a route table can list pages an app doesn't serve, and sends
7
+ that page's share to the pages that answered.
8
+
3
9
  ## 0.1.20 (2026-10-09)
4
10
 
5
11
  - `deployangel exercise --url=<production URL>` sends a release's exercise
data/README.md CHANGED
@@ -471,8 +471,9 @@ path parameters, spreading any request shortfall across them (at most 200
471
471
  requests, about 5 a second, as `DeployAngel-Exercise`), then records what it
472
472
  sent on the release, where the page lists it under "Exercised from your
473
473
  side". It skips and names routes that change data or need a path
474
- parameter. A "CLI & coding agents" token can run it. `--dry-run` shows what
475
- it would send.
474
+ parameter, and stops requesting a page after its first 404, sending its
475
+ share to the pages that answered. A "CLI & coding agents" token can run
476
+ it. `--dry-run` shows what it would send.
476
477
 
477
478
  ### Suggested instructions for your coding agent
478
479
 
@@ -16,6 +16,9 @@ module DeployAngel
16
16
  TIMEOUT = 10
17
17
  # Stop when the app isn't answering, rather than send the rest into it.
18
18
  MAX_CONSECUTIVE_ERRORS = 5
19
+ # A route table can list pages an app doesn't serve, like the edit page
20
+ # of a resource that has none. One answer like this is enough to know.
21
+ NOT_SERVED = [ 404, 405, 410 ].freeze
19
22
  # Rails' :id and *path, Django's <int:pk>, FastAPI's {id}.
20
23
  PARAMETER = /\/[:*{<]/
21
24
 
@@ -51,27 +54,34 @@ module DeployAngel
51
54
  [ targets, skipped ]
52
55
  end
53
56
 
57
+ # A route whose first request says it isn't served gets no more; its
58
+ # share goes to the routes that answered.
54
59
  def run(exercise_plan)
55
60
  targets, skipped = plan(exercise_plan)
56
- routes = []
57
- errors_in_a_row = 0
58
- sent = 0
61
+ @statuses = Hash.new { |hash, key| hash[key] = Hash.new(0) }
62
+ @sent = 0
63
+ @errors_in_a_row = 0
64
+ leftover = 0
65
+ answered = []
59
66
  targets.each do |target|
60
- statuses = Hash.new(0)
61
- target.count.times do
62
- break if errors_in_a_row >= MAX_CONSECUTIVE_ERRORS
63
-
64
- @sleeper.call(INTERVAL) if sent.positive?
65
- status = @requester.call(url_for(target.path))
66
- sent += 1
67
- kind = status ? "#{status / 100}xx" : "error"
68
- statuses[kind] += 1
69
- errors_in_a_row = kind == "error" ? errors_in_a_row + 1 : 0
67
+ target.count.times do |i|
68
+ break if unreachable?
69
+
70
+ status = send_request(target)
71
+ if i.zero? && NOT_SERVED.include?(status)
72
+ leftover += target.count - 1
73
+ break
74
+ end
70
75
  end
71
- requests = statuses.values.sum
72
- routes << { "key" => target.key, "requests" => requests, "statuses" => statuses.to_h } if requests.positive?
76
+ answered << target if @statuses[target.key].keys.intersect?(%w[2xx 3xx])
77
+ end
78
+ leftover.times { |i| send_request(answered[i % answered.size]) unless unreachable? } if answered.any?
79
+
80
+ routes = targets.filter_map do |target|
81
+ statuses = @statuses.fetch(target.key, nil) or next
82
+ { "key" => target.key, "requests" => statuses.values.sum, "statuses" => statuses.to_h }
73
83
  end
74
- Result.new(routes: routes, skipped: skipped, sent: sent, unreachable: errors_in_a_row >= MAX_CONSECUTIVE_ERRORS)
84
+ Result.new(routes: routes, skipped: skipped, sent: @sent, unreachable: unreachable?)
75
85
  end
76
86
 
77
87
  def url_for(path)
@@ -82,6 +92,20 @@ module DeployAngel
82
92
  end
83
93
 
84
94
  private
95
+ def send_request(target)
96
+ @sleeper.call(INTERVAL) if @sent.positive?
97
+ status = @requester.call(url_for(target.path))
98
+ @sent += 1
99
+ kind = status ? "#{status / 100}xx" : "error"
100
+ @statuses[target.key][kind] += 1
101
+ @errors_in_a_row = kind == "error" ? @errors_in_a_row + 1 : 0
102
+ status
103
+ end
104
+
105
+ def unreachable?
106
+ @errors_in_a_row >= MAX_CONSECUTIVE_ERRORS
107
+ end
108
+
85
109
  # Requests the release is short of overall, spread across the routes,
86
110
  # or sent to the home page when the plan names no route to send them to.
87
111
  def spread_shortfall(targets, requests)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeployAngel
4
- VERSION = "0.1.20"
4
+ VERSION = "0.1.21"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deployangel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.20
4
+ version: 0.1.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Owens