cronwatch 0.14.0 → 0.14.1

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: c5dd574dbbc8737a8bc69218d0001eee51f48c3a736eb9ef41e1633a32f5f6c2
4
- data.tar.gz: a9f090cdd69a8c19b3c60e4ca2682e68eff5f4a88f70476d9a62b379444ac274
3
+ metadata.gz: d0a9fcfe8eb1b18e8afd7b80e2b41e62569bf606db798252d2305f9f353235ff
4
+ data.tar.gz: 12626c9fc256348658a3843ac1704b97ef7cd4fde62f289e0c570c174dd29704
5
5
  SHA512:
6
- metadata.gz: dd9d2b10aefac9b694fd8053781003c364c6113800a34ead88db2ed9fbbde32805b5d4d39a29a571ed528e9d11044164e1d1f1dc6899946c1d43647bf233e7a4
7
- data.tar.gz: e00dbf003d44c0c3d280090d06deb00e397edcff50f9cab6bf65f45215f318521701700bf6fd94ec7dd47a835647c6dc18352bb4ffe54ecd9aa205c5723e65d0
6
+ metadata.gz: 4d9274543edac3d3400762e5863b08a2b9cf8a818b134996e28a292cfe802552e6ecffbc68b18a7a4a9258499e476e7e7982a546b4b204ebc5f146829e14cebc
7
+ data.tar.gz: ab1c1ef624f8e94f56eb73c2be364ebf9b799e1b7bceef3a69e4398604e72c1eb63252f894aec82bdb4dfc61d9b9803048a02564dd60d807a273cfde62b0b364
@@ -428,7 +428,7 @@ module Cronwatch
428
428
  update_state(run.job) { |before| [Evaluate.hold_run(Evaluate.on_run_start(definition, run, before, now), run.id), nil] }
429
429
  return [] if run.status == :running
430
430
 
431
- finish_run(definition, run, now)
431
+ finish_run(definition, run, now, pending_only: true)
432
432
  end
433
433
 
434
434
  # Hands an error to on_error, as a source reports what went wrong. See #report.
@@ -919,6 +919,7 @@ module Cronwatch
919
919
  end
920
920
  return [] if !%i[running timeout].include?(stored.status) || run.status == :running
921
921
 
922
+ tracked = (read_state(run.job).pending_runs || []).include?(run.id)
922
923
  late, ignored = claim_finish(run)
923
924
  if ignored
924
925
  report(RuntimeError.new("run #{run.id} of #{run.job} #{ignored}; ignored"), "recording #{run.job}")
@@ -931,7 +932,7 @@ module Cronwatch
931
932
  end
932
933
  return [] if late && run.status != :ok
933
934
 
934
- finish_run(definition, run, now)
935
+ finish_run(definition, run, now, pending_only: tracked && !late)
935
936
  end
936
937
 
937
938
  # Takes a run stored without judging out of the job's pending runs, so a
@@ -1028,13 +1029,14 @@ module Cronwatch
1028
1029
  return "belongs to job \"#{stored.job}\"" if stored.job != run.job
1029
1030
  end
1030
1031
  end
1032
+ tracked = (read_state(run.job).pending_runs || []).include?(run.id)
1031
1033
  late, ignored = claim_finish(run)
1032
1034
  return ignored if ignored
1033
1035
 
1034
1036
  if !judge
1035
1037
  release_unjudged(run)
1036
1038
  elsif !late || run.status == :ok
1037
- finish_run(Serialize.to_stored(definition), run, finished_at)
1039
+ finish_run(Serialize.to_stored(definition), run, finished_at, pending_only: tracked && !late)
1038
1040
  end
1039
1041
  nil
1040
1042
  end
@@ -1250,7 +1252,7 @@ module Cronwatch
1250
1252
  end
1251
1253
  next if run.status == :running || !run.finished_at.is_a?(Numeric) || run.finished_at > at - Evaluate::SEND_LEASE_MS
1252
1254
 
1253
- alerts.concat(finish_run(stored.definition, run, at))
1255
+ alerts.concat(finish_run(stored.definition, run, at, pending_only: true))
1254
1256
  rescue StandardError => e
1255
1257
  report(e, "evaluating run #{id} of #{stored.name}")
1256
1258
  end
@@ -1260,11 +1262,12 @@ module Cronwatch
1260
1262
  # Evaluate a finished run (ok, failed, or timed out by a check), already
1261
1263
  # written, against the job's state and send what that produces. The
1262
1264
  # alerts are written with that state (see outbox). Never raises.
1263
- def finish_run(definition, run, at)
1265
+ def finish_run(definition, run, at, pending_only: false)
1264
1266
  held = nil
1265
1267
  begin
1266
1268
  past = nil
1267
1269
  _, held = update_state(run.job) do |previous|
1270
+ next [previous, Outbox.new([], 0)] if pending_only && !(previous.pending_runs || []).include?(run.id)
1268
1271
  past ||= history(run)
1269
1272
  before = Evaluate.release_run(previous, run.id)
1270
1273
  outbox(Evaluate.apply_silence(before, Evaluate.on_run_finish(definition, run, before, past, at), at), definition, at)
data/lib/cronwatch/job.rb CHANGED
@@ -100,7 +100,7 @@ module Cronwatch
100
100
  class RunRecorder
101
101
  # Lines are dropped from the front once the output is well past the cap;
102
102
  # Output.redact_and_cap trims it exactly at the end.
103
- KEEP = 64 * 1024
103
+ KEEP = Output::CAP + Output::REDACT_EDGE + 2
104
104
 
105
105
  attr_reader :context, :signal
106
106
 
@@ -111,7 +111,9 @@ module Cronwatch
111
111
  # dropped from @lines: what expect_text needs once the output runs long.
112
112
  @head = []
113
113
  @head_size = 0
114
- @dropped = false
114
+ @whole = ""
115
+ @tail = ""
116
+ @logged = false
115
117
  @metrics = {}
116
118
  @lock = Mutex.new
117
119
  @signal = AbortSignal.new(timeout_ms)
@@ -121,15 +123,26 @@ module Cronwatch
121
123
  def log(line)
122
124
  length = JS.length16(line)
123
125
  @lock.synchronize do
126
+ separator = @logged ? "\n" : ""
127
+ @logged = true
128
+ if @whole
129
+ @whole = JS.length16(@whole) + separator.length + length <= 2 * Output::CAP ? @whole + separator + line : nil
130
+ end
131
+ @tail = JS.tail16(@tail + separator + JS.tail16(line, Output::CAP), Output::CAP).dup
124
132
  if @head_size < Output::CAP
125
- @head << line
133
+ @head << JS.head16(line, Output::CAP - @head_size).dup
126
134
  @head_size += length + 1
127
135
  end
136
+ line = Output.strip_nul(line)
137
+ length = JS.length16(line)
138
+ if length >= KEEP
139
+ line = JS.tail16(line, KEEP - 1).dup
140
+ length = JS.length16(line)
141
+ end
128
142
  @lines << line
129
143
  @size += length + 1
130
144
  while @size > KEEP && @lines.length > 1
131
145
  @size -= JS.length16(@lines.shift) + 1
132
- @dropped = true
133
146
  end
134
147
  end
135
148
  end
@@ -138,7 +151,7 @@ module Cronwatch
138
151
  @lock.synchronize { @metrics[name] = value }
139
152
  end
140
153
 
141
- # The lines still held (past 64 KB the oldest are let go), joined and not
154
+ # The lines still held (past the bounded redaction window the oldest are let go), joined and not
142
155
  # yet capped: the client redacts them first, then caps them
143
156
  # (Output.redact_and_cap).
144
157
  def output
@@ -152,10 +165,9 @@ module Cronwatch
152
165
  @lock.synchronize do
153
166
  next nil if @lines.empty?
154
167
 
155
- all = @lines.join("\n")
156
- next all if !@dropped && JS.length16(all) <= 2 * Output::CAP
168
+ next @whole if @whole
157
169
 
158
- "#{JS.head16(@head.join("\n"), Output::CAP)}\n#{JS.tail16(all, Output::CAP)}"
170
+ "#{JS.head16(@head.join("\n"), Output::CAP)}\n#{@tail}"
159
171
  end
160
172
  end
161
173
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Cronwatch
4
4
  # Kept in step with @cronwatch/sdk.
5
- VERSION = "0.14.0"
5
+ VERSION = "0.14.1"
6
6
  end
@@ -264,6 +264,7 @@ module Cronwatch
264
264
  # the URL and every access log. Cross-site posts were refused above.
265
265
  if @token && method == "POST" && path == "/signin"
266
266
  body = read_body(request)
267
+ return html(HTML.message_page("Bad request", "Malformed request body", base), 400) if body.equal?(BAD_BODY)
267
268
  # A body too large is no token at all.
268
269
  sent = body.equal?(TOO_LARGE) ? nil : body["token"]
269
270
  return sign_in_page(base) if sent.nil? || !HTTP.constant_time_equal?(sent, @token)
@@ -341,6 +342,7 @@ module Cronwatch
341
342
  if action == "silence"
342
343
  body = read_body(request)
343
344
  return html(HTML.message_page("Not silenced", "The request was too large.", base), 413) if body.equal?(TOO_LARGE)
345
+ return html(HTML.message_page("Not silenced", "Malformed request body", base), 400) if body.equal?(BAD_BODY)
344
346
 
345
347
  begin
346
348
  duration = silence_duration(body["for"])
@@ -388,6 +390,7 @@ module Cronwatch
388
390
  if rest[2] == "silence"
389
391
  body = read_body(request)
390
392
  return api({ ok: false, error: "Request body too large" }, 413) if body.equal?(TOO_LARGE)
393
+ return api({ ok: false, error: "Malformed request body" }, 400) if body.equal?(BAD_BODY)
391
394
 
392
395
  begin
393
396
  duration = silence_duration(body.key?("for") ? body["for"] : request.query("for"))
@@ -530,7 +533,8 @@ module Cronwatch
530
533
 
531
534
  # A request body past Request::MAX_BODY, which read_body answers with.
532
535
  TOO_LARGE = Object.new.freeze
533
- private_constant :TOO_LARGE
536
+ BAD_BODY = Object.new.freeze
537
+ private_constant :TOO_LARGE, :BAD_BODY
534
538
 
535
539
  # The form fields or JSON object of a request, each value as String(value)
536
540
  # gives it in JavaScript, or TOO_LARGE for a body past Request::MAX_BODY.
@@ -546,13 +550,13 @@ module Cronwatch
546
550
  if json
547
551
  data = JSON.parse(Output.utf8(request.body).delete_prefix(""))
548
552
  return data.to_h { |k, v| [k.to_s, js_string(v)] } if data.is_a?(Hash)
549
- return data.each_with_index.to_h { |v, i| [i.to_s, js_string(v)] } if data.is_a?(Array)
553
+ return BAD_BODY
550
554
  else
551
555
  return request.form.transform_values { |v| form_value(v) }
552
556
  end
553
557
  {}
554
558
  rescue StandardError
555
- {}
559
+ BAD_BODY
556
560
  end
557
561
 
558
562
  # String(value) for a parsed JSON value.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cronwatch
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon C. Phillips