cosmonats 0.8.0 → 0.9.0

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.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -11
  3. data/lib/cosmo/api/counter.rb +1 -1
  4. data/lib/cosmo/api/cron/entry.rb +10 -11
  5. data/lib/cosmo/api/cron.rb +3 -3
  6. data/lib/cosmo/api/job.rb +1 -1
  7. data/lib/cosmo/api/kv.rb +3 -3
  8. data/lib/cosmo/api/stats/busy.rb +5 -0
  9. data/lib/cosmo/api/stats/metrics.rb +3 -3
  10. data/lib/cosmo/api/stats.rb +15 -0
  11. data/lib/cosmo/api/stream.rb +7 -2
  12. data/lib/cosmo/client.rb +10 -13
  13. data/lib/cosmo/job/data.rb +1 -1
  14. data/lib/cosmo/job/processor.rb +5 -6
  15. data/lib/cosmo/utils.rb +0 -1
  16. data/lib/cosmo/version.rb +1 -1
  17. data/lib/cosmo/web/assets/app.css +271 -629
  18. data/lib/cosmo/web/assets/app.js +158 -0
  19. data/lib/cosmo/web/assets/fonts/OFL.txt +92 -0
  20. data/lib/cosmo/web/assets/fonts/geist-mono.woff2 +0 -0
  21. data/lib/cosmo/web/assets/fonts/geist.woff2 +0 -0
  22. data/lib/cosmo/web/assets/logo.svg +10 -0
  23. data/lib/cosmo/web/cache.rb +75 -0
  24. data/lib/cosmo/web/chart.rb +62 -38
  25. data/lib/cosmo/web/circuit.rb +55 -0
  26. data/lib/cosmo/web/context.rb +0 -4
  27. data/lib/cosmo/web/controllers/application.rb +16 -0
  28. data/lib/cosmo/web/controllers/batches.rb +1 -0
  29. data/lib/cosmo/web/controllers/crons.rb +9 -2
  30. data/lib/cosmo/web/controllers/jobs.rb +50 -59
  31. data/lib/cosmo/web/controllers/metrics.rb +1 -0
  32. data/lib/cosmo/web/controllers/overview.rb +70 -0
  33. data/lib/cosmo/web/controllers/processes.rb +33 -0
  34. data/lib/cosmo/web/controllers/streams.rb +26 -28
  35. data/lib/cosmo/web/helpers/application.rb +57 -0
  36. data/lib/cosmo/web/renderer.rb +19 -6
  37. data/lib/cosmo/web/throughput.rb +104 -0
  38. data/lib/cosmo/web/views/batches/_table.erb +42 -54
  39. data/lib/cosmo/web/views/batches/index.erb +3 -10
  40. data/lib/cosmo/web/views/crons/_table.erb +32 -53
  41. data/lib/cosmo/web/views/crons/index.erb +2 -9
  42. data/lib/cosmo/web/views/jobs/_args.erb +5 -0
  43. data/lib/cosmo/web/views/jobs/_busy.erb +25 -121
  44. data/lib/cosmo/web/views/jobs/_dead.erb +42 -82
  45. data/lib/cosmo/web/views/jobs/_enqueued.erb +33 -113
  46. data/lib/cosmo/web/views/jobs/_panel.erb +29 -0
  47. data/lib/cosmo/web/views/jobs/_queue_row.erb +18 -0
  48. data/lib/cosmo/web/views/jobs/_queues.erb +20 -0
  49. data/lib/cosmo/web/views/jobs/_scheduled.erb +20 -54
  50. data/lib/cosmo/web/views/jobs/index.erb +4 -17
  51. data/lib/cosmo/web/views/layout.erb +73 -22
  52. data/lib/cosmo/web/views/metrics/_chart.erb +37 -32
  53. data/lib/cosmo/web/views/metrics/_panel.erb +3 -3
  54. data/lib/cosmo/web/views/metrics/_table.erb +24 -40
  55. data/lib/cosmo/web/views/metrics/index.erb +8 -16
  56. data/lib/cosmo/web/views/overview/_badges.erb +3 -0
  57. data/lib/cosmo/web/views/overview/_kpis.erb +19 -0
  58. data/lib/cosmo/web/views/overview/_running.erb +18 -0
  59. data/lib/cosmo/web/views/overview/_throughput.erb +50 -0
  60. data/lib/cosmo/web/views/overview/_workers.erb +25 -0
  61. data/lib/cosmo/web/views/overview/index.erb +28 -0
  62. data/lib/cosmo/web/views/processes/_table.erb +67 -0
  63. data/lib/cosmo/web/views/processes/index.erb +1 -0
  64. data/lib/cosmo/web/views/shared/_pager.erb +22 -0
  65. data/lib/cosmo/web/views/shared/unavailable.erb +7 -0
  66. data/lib/cosmo/web/views/streams/_info.erb +28 -86
  67. data/lib/cosmo/web/views/streams/_list.erb +7 -0
  68. data/lib/cosmo/web/views/streams/_pause_banner.erb +8 -16
  69. data/lib/cosmo/web/views/streams/_stream.erb +52 -0
  70. data/lib/cosmo/web/views/streams/index.erb +3 -11
  71. data/lib/cosmo/web/views/streams/info.erb +3 -11
  72. data/lib/cosmo/web.rb +66 -35
  73. data/sig/cosmo/api/cron/entry.rbs +2 -1
  74. data/sig/cosmo/api/stats/busy.rbs +2 -0
  75. data/sig/cosmo/api/stats/metrics.rbs +1 -1
  76. data/sig/cosmo/api/stats.rbs +4 -0
  77. data/sig/cosmo/api/stream.rbs +2 -0
  78. data/sig/cosmo/client.rbs +1 -0
  79. metadata +31 -18
  80. data/lib/cosmo/utils/overrides.rb +0 -206
  81. data/lib/cosmo/utils/warnings.rb +0 -17
  82. data/lib/cosmo/web/controllers/actions.rb +0 -16
  83. data/lib/cosmo/web/views/actions/index.erb +0 -7
  84. data/lib/cosmo/web/views/jobs/_processes.erb +0 -145
  85. data/lib/cosmo/web/views/jobs/_stats.erb +0 -77
  86. data/lib/cosmo/web/views/jobs/_tabs.erb +0 -14
  87. data/lib/cosmo/web/views/jobs/busy.erb +0 -18
  88. data/lib/cosmo/web/views/jobs/dead.erb +0 -18
  89. data/lib/cosmo/web/views/jobs/enqueued.erb +0 -18
  90. data/lib/cosmo/web/views/jobs/scheduled.erb +0 -18
  91. data/lib/cosmo/web/views/streams/_stream_row.erb +0 -42
  92. data/lib/cosmo/web/views/streams/_table.erb +0 -25
data/lib/cosmo/web.rb CHANGED
@@ -3,12 +3,16 @@
3
3
  require "rack"
4
4
  require "json"
5
5
  require "erb"
6
+ require "zlib"
7
+ require "cosmo/web/cache"
8
+ require "cosmo/web/circuit"
6
9
  require "cosmo/web/context"
7
10
  require "cosmo/web/renderer"
8
11
  require "cosmo/web/controllers/application"
12
+ require "cosmo/web/controllers/overview"
9
13
  require "cosmo/web/controllers/jobs"
14
+ require "cosmo/web/controllers/processes"
10
15
  require "cosmo/web/controllers/streams"
11
- require "cosmo/web/controllers/actions"
12
16
  require "cosmo/web/controllers/crons"
13
17
  require "cosmo/web/controllers/batches"
14
18
  require "cosmo/web/controllers/metrics"
@@ -17,57 +21,84 @@ module Cosmo
17
21
  class Web
18
22
  include Renderer
19
23
 
24
+ FONT_HEADERS = { "cache-control" => "public, max-age=31536000, immutable" }.freeze
25
+
20
26
  def self.call(env)
21
27
  new.call(env)
22
28
  end
23
29
 
24
- def call(env) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
30
+ # Serves the UI. An htmx refresh is shared through Cache with concurrent and recent identical ones; any other
31
+ # method than GET changes something, so it empties the cache. While Circuit is open, anything but an asset
32
+ # answers 503 at once instead of waiting on NATS.
33
+ def call(env) # rubocop:disable Metrics/AbcSize
25
34
  @request = Rack::Request.new(env)
35
+ return route if @request.path_info.start_with?("/assets/", "/favicon.ico")
36
+ return guarded.tap { Cache.instance.clear } unless @request.get?
37
+ return guarded unless @request.get_header("HTTP_HX_REQUEST") == "true"
38
+
39
+ status, headers, body = Cache.instance.fetch(@request.fullpath) { guarded }
40
+ [status, headers.dup, body.dup]
41
+ rescue *Circuit::ERRORS
42
+ Circuit.instance.trip!
43
+ unavailable
44
+ end
45
+
46
+ private
47
+
48
+ def route # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
26
49
  path = @request.path_info
27
50
  method = @request.request_method
28
51
 
29
52
  response = case [method.downcase.to_sym, path]
30
- in [:get, "/"] then redirect_to("/jobs")
31
- in [:get, "/jobs"] then [Controllers::Jobs, :index]
32
- in [:get, "/jobs/scheduled"] then [Controllers::Jobs, :scheduled]
33
- in [:get, "/jobs/dead"] then [Controllers::Jobs, :dead]
34
- in [:get, "/jobs/busy"] then [Controllers::Jobs, :busy]
35
- in [:get, "/jobs/enqueued"] then [Controllers::Jobs, :enqueued]
36
- in [:patch, %r{/jobs/retry/\d+}] then [Controllers::Jobs, :retry]
37
- in [:delete, %r{/jobs/delete/\d+}] then [Controllers::Jobs, :delete]
38
- in [:delete, %r{/jobs/enqueued/\d+}] then [Controllers::Jobs, :delete_enqueued]
39
- in [:get, "/jobs/_stats"] then [Controllers::Jobs, :_stats]
40
- in [:get, "/jobs/_scheduled"] then [Controllers::Jobs, :_scheduled]
41
- in [:get, "/jobs/_dead"] then [Controllers::Jobs, :_dead]
42
- in [:get, "/jobs/_busy"] then [Controllers::Jobs, :_busy]
43
- in [:get, "/jobs/_processes"] then [Controllers::Jobs, :_processes]
44
- in [:get, "/jobs/_enqueued"] then [Controllers::Jobs, :_enqueued]
45
- in [:get, "/streams"] then [Controllers::Streams, :index]
46
- in [:get, "/streams/info"] then [Controllers::Streams, :info]
47
- in [:get, "/streams/_table"] then [Controllers::Streams, :_table]
48
- in [:get, "/streams/_info"] then [Controllers::Streams, :_info]
49
- in [:patch, "/streams/pause"] then [Controllers::Streams, :pause]
50
- in [:patch, "/streams/unpause"] then [Controllers::Streams, :unpause]
51
- in [:get, "/crons"] then [Controllers::Crons, :index]
52
- in [:get, "/crons/_table"] then [Controllers::Crons, :_table]
53
- in [:delete, "/crons/delete"] then [Controllers::Crons, :delete]
54
- in [:post, "/crons/run"] then [Controllers::Crons, :run_now]
55
- in [:get, "/metrics"] then [Controllers::Metrics, :index]
56
- in [:get, "/metrics/_panel"] then [Controllers::Metrics, :_panel]
57
- in [:get, "/batches"] then [Controllers::Batches, :index]
58
- in [:get, "/batches/_table"] then [Controllers::Batches, :_table]
59
- in [:get, "/actions"] then [Controllers::Actions, :index]
53
+ in [:get, "/" | ""] then [Controllers::Overview, :index]
54
+ in [:get, "/_kpis"] then [Controllers::Overview, :_kpis]
55
+ in [:get, "/_throughput"] then [Controllers::Overview, :_throughput]
56
+ in [:get, "/_workers"] then [Controllers::Overview, :_workers]
57
+ in [:get, "/_running"] then [Controllers::Overview, :_running]
58
+ in [:get, "/_badges"] then [Controllers::Overview, :_badges]
59
+ in [:get, "/jobs"] then [Controllers::Jobs, :index]
60
+ in [:get, "/jobs/busy"] then [Controllers::Jobs, :busy]
61
+ in [:get, "/jobs/enqueued"] then [Controllers::Jobs, :enqueued]
62
+ in [:get, "/jobs/scheduled"] then [Controllers::Jobs, :scheduled]
63
+ in [:get, "/jobs/dead"] then [Controllers::Jobs, :dead]
64
+ in [:get, "/jobs/_queues"] then [Controllers::Jobs, :_queues]
65
+ in [:patch, %r{/jobs/retry/\d+}] then [Controllers::Jobs, :retry]
66
+ in [:delete, %r{/jobs/delete/\d+}] then [Controllers::Jobs, :delete]
67
+ in [:delete, %r{/jobs/enqueued/\d+}] then [Controllers::Jobs, :delete_enqueued]
68
+ in [:get, "/processes"] then [Controllers::Processes, :index]
69
+ in [:get, "/streams"] then [Controllers::Streams, :index]
70
+ in [:get, "/streams/info"] then [Controllers::Streams, :info]
71
+ in [:patch, "/streams/pause"] then [Controllers::Streams, :pause]
72
+ in [:patch, "/streams/unpause"] then [Controllers::Streams, :unpause]
73
+ in [:get, "/crons"] then [Controllers::Crons, :index]
74
+ in [:get, "/crons/_table"] then [Controllers::Crons, :_table]
75
+ in [:delete, "/crons/delete"] then [Controllers::Crons, :delete]
76
+ in [:post, "/crons/run"] then [Controllers::Crons, :run_now]
77
+ in [:get, "/metrics"] then [Controllers::Metrics, :index]
78
+ in [:get, "/metrics/_panel"] then [Controllers::Metrics, :_panel]
79
+ in [:get, "/batches"] then [Controllers::Batches, :index]
80
+ in [:get, "/batches/_table"] then [Controllers::Batches, :_table]
60
81
  in [:get, "/assets/htmx.min.js.gz"] then serve("htmx.2.0.8.min.js.gz",
61
82
  "application/javascript",
62
83
  { "content-encoding" => "gzip" })
63
- in [:get, "/assets/app.css"] then serve("app.css", "text/css")
64
- in [:get, "/favicon.ico"] then no_content
84
+ in [:get, "/assets/app.css"] then serve("app.css", "text/css")
85
+ in [:get, "/assets/app.js"] then serve("app.js", "application/javascript")
86
+ in [:get, "/assets/logo.svg"] then serve("logo.svg", "image/svg+xml")
87
+ in [:get, "/assets/fonts/geist.woff2"] then serve("fonts/geist.woff2", "font/woff2", FONT_HEADERS)
88
+ in [:get, "/assets/fonts/geist-mono.woff2"] then serve("fonts/geist-mono.woff2", "font/woff2", FONT_HEADERS)
89
+ in [:get, "/favicon.ico"] then redirect_to("/assets/logo.svg", 301)
65
90
  else not_found
66
91
  end
67
92
  handler(response)
68
93
  end
69
94
 
70
- private
95
+ def guarded
96
+ Circuit.instance.open? ? unavailable : route
97
+ end
98
+
99
+ def unavailable
100
+ Controllers::Application.new(@request).unavailable
101
+ end
71
102
 
72
103
  def handler(response)
73
104
  if response[0].is_a?(Class)
@@ -19,7 +19,8 @@ module Cosmo
19
19
  def schedule_subject: () -> ::String
20
20
  def target_subject: () -> ::String
21
21
  def dispatch_subject: () -> ::String
22
- def schedule_headers: () -> Hash[::String, ::String]
22
+ def schedule: () -> Hash[Symbol, ::String]
23
+ def headers: () -> Hash[::String, ::String]
23
24
  def job_name: () -> ::String
24
25
  def job_payload: () -> ::String
25
26
  def as_json: () -> Hash[Symbol, untyped]
@@ -20,6 +20,8 @@ module Cosmo
20
20
 
21
21
  def list: (?page: ::Integer?, ?limit: ::Integer) -> Array[Hash[Symbol, untyped]]
22
22
 
23
+ def by_stream: () -> Hash[::String, ::Integer]
24
+
23
25
  private
24
26
 
25
27
  def heartbeat_loop: () -> void
@@ -23,7 +23,7 @@ module Cosmo
23
23
 
24
24
  def summary: (?days: Integer) -> Array[Hash[Symbol, untyped]]
25
25
 
26
- def daily: (::String job_class, ?days: Integer) -> Array[Hash[Symbol, untyped]]
26
+ def daily: (?::String? job_class, ?days: Integer) -> Array[Hash[Symbol, untyped]]
27
27
 
28
28
  private
29
29
 
@@ -16,6 +16,10 @@ module Cosmo
16
16
  def self.scheduled: () -> ::Integer
17
17
 
18
18
  def self.dead: () -> ::Integer
19
+
20
+ def self.queues: () -> Array[Hash[Symbol, untyped]]
21
+
22
+ def self.queue: (Stream stream, ?Hash[::String, ::Integer] busy) -> Hash[Symbol, untyped]
19
23
  end
20
24
  end
21
25
  end
@@ -26,6 +26,8 @@ module Cosmo
26
26
 
27
27
  def retries: () -> ::Integer
28
28
 
29
+ def consumers: () -> Array[Hash[::String, untyped]]
30
+
29
31
  def each: () { (Job) -> void } -> void
30
32
 
31
33
  def messages: (?page: ::Integer?, ?limit: ::Integer?) -> Array[Job]
data/sig/cosmo/client.rbs CHANGED
@@ -2,6 +2,7 @@ module Cosmo
2
2
  class Client
3
3
  JS_TIMEOUT: ::Integer
4
4
  CONNECT_TIMEOUT: ::Integer
5
+ MAX_RECONNECT_ATTEMPTS: ::Integer
5
6
 
6
7
  attr_reader name: ::String
7
8
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cosmonats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Vorotilin
@@ -43,20 +43,20 @@ dependencies:
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: '2.5'
46
+ version: '2.7'
47
47
  - - "<"
48
48
  - !ruby/object:Gem::Version
49
- version: '2.7'
49
+ version: '2.8'
50
50
  type: :runtime
51
51
  prerelease: false
52
52
  version_requirements: !ruby/object:Gem::Requirement
53
53
  requirements:
54
54
  - - ">="
55
55
  - !ruby/object:Gem::Version
56
- version: '2.5'
56
+ version: '2.7'
57
57
  - - "<"
58
58
  - !ruby/object:Gem::Version
59
- version: '2.7'
59
+ version: '2.8'
60
60
  description: Lightweight background and stream processing for Ruby
61
61
  email:
62
62
  - d.vorotilin@gmail.com
@@ -131,54 +131,67 @@ files:
131
131
  - lib/cosmo/utils/duration.rb
132
132
  - lib/cosmo/utils/hash.rb
133
133
  - lib/cosmo/utils/json.rb
134
- - lib/cosmo/utils/overrides.rb
135
134
  - lib/cosmo/utils/signal.rb
136
135
  - lib/cosmo/utils/stopwatch.rb
137
136
  - lib/cosmo/utils/string.rb
138
137
  - lib/cosmo/utils/thread_pool.rb
139
138
  - lib/cosmo/utils/ttl_cache.rb
140
- - lib/cosmo/utils/warnings.rb
141
139
  - lib/cosmo/version.rb
142
140
  - lib/cosmo/web.rb
143
141
  - lib/cosmo/web/assets/app.css
142
+ - lib/cosmo/web/assets/app.js
143
+ - lib/cosmo/web/assets/fonts/OFL.txt
144
+ - lib/cosmo/web/assets/fonts/geist-mono.woff2
145
+ - lib/cosmo/web/assets/fonts/geist.woff2
144
146
  - lib/cosmo/web/assets/htmx.2.0.8.min.js.gz
147
+ - lib/cosmo/web/assets/logo.svg
148
+ - lib/cosmo/web/cache.rb
145
149
  - lib/cosmo/web/chart.rb
150
+ - lib/cosmo/web/circuit.rb
146
151
  - lib/cosmo/web/context.rb
147
- - lib/cosmo/web/controllers/actions.rb
148
152
  - lib/cosmo/web/controllers/application.rb
149
153
  - lib/cosmo/web/controllers/batches.rb
150
154
  - lib/cosmo/web/controllers/crons.rb
151
155
  - lib/cosmo/web/controllers/jobs.rb
152
156
  - lib/cosmo/web/controllers/metrics.rb
157
+ - lib/cosmo/web/controllers/overview.rb
158
+ - lib/cosmo/web/controllers/processes.rb
153
159
  - lib/cosmo/web/controllers/streams.rb
154
160
  - lib/cosmo/web/helpers/application.rb
155
161
  - lib/cosmo/web/renderer.rb
156
- - lib/cosmo/web/views/actions/index.erb
162
+ - lib/cosmo/web/throughput.rb
157
163
  - lib/cosmo/web/views/batches/_table.erb
158
164
  - lib/cosmo/web/views/batches/index.erb
159
165
  - lib/cosmo/web/views/crons/_table.erb
160
166
  - lib/cosmo/web/views/crons/index.erb
167
+ - lib/cosmo/web/views/jobs/_args.erb
161
168
  - lib/cosmo/web/views/jobs/_busy.erb
162
169
  - lib/cosmo/web/views/jobs/_dead.erb
163
170
  - lib/cosmo/web/views/jobs/_enqueued.erb
164
- - lib/cosmo/web/views/jobs/_processes.erb
171
+ - lib/cosmo/web/views/jobs/_panel.erb
172
+ - lib/cosmo/web/views/jobs/_queue_row.erb
173
+ - lib/cosmo/web/views/jobs/_queues.erb
165
174
  - lib/cosmo/web/views/jobs/_scheduled.erb
166
- - lib/cosmo/web/views/jobs/_stats.erb
167
- - lib/cosmo/web/views/jobs/_tabs.erb
168
- - lib/cosmo/web/views/jobs/busy.erb
169
- - lib/cosmo/web/views/jobs/dead.erb
170
- - lib/cosmo/web/views/jobs/enqueued.erb
171
175
  - lib/cosmo/web/views/jobs/index.erb
172
- - lib/cosmo/web/views/jobs/scheduled.erb
173
176
  - lib/cosmo/web/views/layout.erb
174
177
  - lib/cosmo/web/views/metrics/_chart.erb
175
178
  - lib/cosmo/web/views/metrics/_panel.erb
176
179
  - lib/cosmo/web/views/metrics/_table.erb
177
180
  - lib/cosmo/web/views/metrics/index.erb
181
+ - lib/cosmo/web/views/overview/_badges.erb
182
+ - lib/cosmo/web/views/overview/_kpis.erb
183
+ - lib/cosmo/web/views/overview/_running.erb
184
+ - lib/cosmo/web/views/overview/_throughput.erb
185
+ - lib/cosmo/web/views/overview/_workers.erb
186
+ - lib/cosmo/web/views/overview/index.erb
187
+ - lib/cosmo/web/views/processes/_table.erb
188
+ - lib/cosmo/web/views/processes/index.erb
189
+ - lib/cosmo/web/views/shared/_pager.erb
190
+ - lib/cosmo/web/views/shared/unavailable.erb
178
191
  - lib/cosmo/web/views/streams/_info.erb
192
+ - lib/cosmo/web/views/streams/_list.erb
179
193
  - lib/cosmo/web/views/streams/_pause_banner.erb
180
- - lib/cosmo/web/views/streams/_stream_row.erb
181
- - lib/cosmo/web/views/streams/_table.erb
194
+ - lib/cosmo/web/views/streams/_stream.erb
182
195
  - lib/cosmo/web/views/streams/index.erb
183
196
  - lib/cosmo/web/views/streams/info.erb
184
197
  - lib/cosmonats.rb
@@ -1,206 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- Cosmo::Utils::Warnings.silence do
4
- unless NATS::JetStream::API::StreamConfig.members.include?(:allow_msg_schedules)
5
- members = NATS::JetStream::API::StreamConfig.members + %i[allow_msg_counter allow_msg_schedules]
6
- NATS::JetStream::API::StreamConfig = Struct.new(*members, keyword_init: true) do
7
- def initialize(opts = {})
8
- rem = opts.keys - members
9
- opts.delete_if { |k| rem.include?(k) }
10
- super
11
- end
12
- end
13
- end
14
-
15
- unless NATS::JetStream::PubAck.members.include?(:val)
16
- members = NATS::JetStream::PubAck.members + [:val]
17
- NATS::JetStream::PubAck = Struct.new(*members, keyword_init: true)
18
- end
19
- end
20
-
21
- # Upstream bug in nats-pure 2.5.0 and 2.6.0 (https://github.com/nats-io/nats-pure.rb):
22
- # NATS::JetStream::PullSubscription#fetch only assigns its `start_time` local in the
23
- # `batch > 1` branch, but the final "did we time out" check outside the case statement
24
- # reads it unconditionally. For `batch == 1` (what Cosmo::Processor always uses),
25
- # `start_time` is nil there -- normally masked because the `batch == 1` branch has its
26
- # own timeout check (using a properly-set `t`) that raises first.
27
- #
28
- # It only surfaces once two threads call #fetch concurrently on the *same* subscription:
29
- # @pending_queue/wait_for_msgs_cond are shared per-subscription state, and #dispatch's
30
- # `wait_for_msgs_cond.signal` wakes exactly one arbitrary waiter -- not necessarily the
31
- # one whose own request produced that wakeup. A thread woken by someone else's signal
32
- # finds nothing left in the (already-drained) queue, so it skips its own inner timeout
33
- # check and falls into the buggy tail check, raising `TypeError: nil can't be coerced
34
- # into Float` instead of the intended timeout/empty result. Reproduced deterministically
35
- # with N threads fetching in a loop against the same PullSubscription.
36
- #
37
- # Second, separate upstream bug in the same method: the post-wait timeout check below
38
- # (`raise ... if MonotonicTime.since(t) > timeout`) fires unconditionally, unlike every
39
- # other timeout check in this method (all guarded with `msgs.empty? &&`). If the unsynchronized
40
- # pop just above it succeeded in grabbing a real, already-delivered message -- which can happen
41
- # even after `timeout` has technically elapsed, since #dispatch's signal and this thread actually
42
- # resuming are two different moments -- that message is already gone from @pending_queue with
43
- # nowhere else to go, yet gets discarded here and a plain NATS::Timeout raised instead. Callers
44
- # (including Cosmo::Processor#fetch) treat NATS::Timeout as "no messages", so the message is lost
45
- # silently: never processed, never acked, and not redelivered until the consumer's ack_wait expires.
46
- # Reproduces reliably with several threads fetching (batch: 1) concurrently against the same
47
- # subscription, e.g. Cosmo::Job::Processor's thread pool.
48
- #
49
- # Vendored copy of nats-pure's PullSubscription#fetch, kept as close to
50
- # upstream as possible (two one-line fixes, see comments above), so it's easy to diff against the
51
- # next nats-pure release and drop once fixed there. Fixed on nats-pure main by the fetch rewrite
52
- # (#181), recognized by its private #take_pending, in which case this copy is not loaded.
53
- return if NATS::JetStream.const_get(:PullSubscription).private_method_defined?(:take_pending)
54
-
55
- # rubocop:disable all
56
- module NATS
57
- class JetStream
58
- module PullSubscription
59
- def fetch(batch = 1, params = {})
60
- raise ::NATS::JetStream::Error.new("nats: invalid batch size") if batch < 1
61
-
62
- t = MonotonicTime.now
63
- start_time = t # fixes the upstream bug -- see comment above
64
- timeout = params[:timeout] ||= 5
65
- expires = (timeout * 1_000_000_000) - 100_000
66
- next_req = { batch: batch }
67
-
68
- msgs = []
69
- case
70
- when batch == 1
71
- synchronize do
72
- unless @pending_queue.empty?
73
- msg = @pending_queue.pop
74
- @pending_size -= msg.data.size
75
- if JS.is_status_msg(msg)
76
- case msg.header["Status"]
77
- when JS::Status::NoMsgs then nil
78
- when JS::Status::RequestTimeout then nil # Skip
79
- else raise JS.from_msg(msg)
80
- end
81
- else
82
- msgs << msg
83
- end
84
- end
85
- end
86
-
87
- next_req[:expires] = expires
88
- if msgs.empty?
89
- @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
90
- synchronize { wait_for_msgs_cond.wait(timeout) }
91
-
92
- unless @pending_queue.empty?
93
- msg = @pending_queue.pop
94
- @pending_size -= msg.data.size
95
- msgs << msg
96
- end
97
-
98
- raise ::NATS::Timeout.new("nats: fetch timeout") if msgs.empty? && (MonotonicTime.since(t) > timeout)
99
-
100
- if JS.is_status_msg(msgs.first)
101
- msg = msgs.first
102
- case msg.header[JS::Header::Status]
103
- when JS::Status::RequestTimeout then raise NATS::Timeout.new("nats: fetch request timeout")
104
- else raise JS.from_msg(msgs.first)
105
- end
106
- end
107
- end
108
- when batch > 1
109
- synchronize do
110
- if batch <= @pending_queue.size
111
- batch.times do
112
- msg = @pending_queue.pop
113
- @pending_size -= msg.data.size
114
- if JS.is_status_msg(msg)
115
- case msg.header[JS::Header::Status]
116
- when JS::Status::NoMsgs, JS::Status::RequestTimeout then next
117
- else raise JS.from_msg(msg)
118
- end
119
- else
120
- msgs << msg
121
- end
122
- end
123
- return msgs
124
- end
125
- end
126
-
127
- next_req[:no_wait] = true
128
- @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
129
-
130
- start_time = MonotonicTime.now
131
- msg = nil
132
-
133
- synchronize do
134
- wait_for_msgs_cond.wait(timeout)
135
- unless @pending_queue.empty?
136
- msg = @pending_queue.pop
137
- @pending_size -= msg.data.size
138
- end
139
- end
140
-
141
- if !msg.nil? && JS.is_status_msg(msg)
142
- case msg.header[JS::Header::Status]
143
- when JS::Status::NoMsgs
144
- next_req[:expires] = expires
145
- next_req.delete(:no_wait)
146
- @nc.publish(@jsi.nms, JS.next_req_to_json(next_req), @subject)
147
- when JS::Status::RequestTimeout
148
- raise NATS::Timeout.new("nats: fetch request timeout")
149
- else
150
- raise JS.from_msg(msg)
151
- end
152
- else
153
- msgs << msg unless msg.nil?
154
- end
155
-
156
- duration = MonotonicTime.since(start_time)
157
- raise NATS::Timeout.new("nats: fetch timeout") if msgs.empty? && (duration > timeout)
158
-
159
- needed = batch - msgs.count
160
- while (needed > 0) && (MonotonicTime.since(start_time) < timeout)
161
- duration = MonotonicTime.since(start_time)
162
-
163
- synchronize do
164
- if @pending_queue.empty?
165
- deadline = timeout - duration
166
- wait_for_msgs_cond.wait(deadline) if deadline > 0
167
-
168
- duration = MonotonicTime.since(start_time)
169
- if msgs.empty? && @pending_queue.empty? && (duration > timeout)
170
- raise NATS::Timeout.new("nats: fetch timeout")
171
- end
172
- end
173
-
174
- unless @pending_queue.empty?
175
- msg = @pending_queue.pop
176
- @pending_size -= msg.data.size
177
-
178
- if JS.is_status_msg(msg)
179
- case msg.header[JS::Header::Status]
180
- when JS::Status::NoMsgs, JS::Status::RequestTimeout
181
- duration = MonotonicTime.since(start_time)
182
- if duration > timeout
183
- raise NATS::Timeout.new("nats: fetch timeout") if msgs.empty?
184
-
185
- return msgs
186
- end
187
- else
188
- raise JS.from_msg(msg)
189
- end
190
- else
191
- msgs << msg
192
- needed -= 1
193
- end
194
- end
195
- end
196
- end
197
- end
198
-
199
- raise ::NATS::Timeout.new("nats: fetch timeout") if msgs.empty? && (MonotonicTime.since(start_time) > timeout)
200
-
201
- msgs
202
- end
203
- end
204
- end
205
- end
206
- # rubocop:enable all
@@ -1,17 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Cosmo
4
- module Utils
5
- module Warnings
6
- module_function
7
-
8
- def silence
9
- verbose = $VERBOSE
10
- $VERBOSE = nil
11
- yield
12
- ensure
13
- $VERBOSE = verbose
14
- end
15
- end
16
- end
17
- end
@@ -1,16 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "cosmo/web/controllers/application"
4
-
5
- module Cosmo
6
- class Web
7
- module Controllers
8
- class Actions < Application
9
- def index
10
- content_for :title, "Actions"
11
- ok render("actions/index", layout: true)
12
- end
13
- end
14
- end
15
- end
16
- end
@@ -1,7 +0,0 @@
1
- <section>
2
- <header></header>
3
-
4
- <div>
5
- <div class="alert alert-info">Coming soon, stay tuned</div>
6
- </div>
7
- </section>