tina4ruby 3.0.0 → 3.9.2
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 +4 -4
- data/README.md +120 -32
- data/lib/tina4/auth.rb +137 -27
- data/lib/tina4/auto_crud.rb +55 -3
- data/lib/tina4/cli.rb +228 -28
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database.rb +230 -26
- data/lib/tina4/database_result.rb +122 -8
- data/lib/tina4/dev_mailbox.rb +1 -1
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/frond.rb +314 -7
- data/lib/tina4/gallery/queue/meta.json +1 -1
- data/lib/tina4/gallery/queue/src/routes/api/gallery_queue.rb +314 -16
- data/lib/tina4/localization.rb +1 -1
- data/lib/tina4/messenger.rb +111 -33
- data/lib/tina4/middleware.rb +349 -1
- data/lib/tina4/migration.rb +132 -11
- data/lib/tina4/orm.rb +149 -18
- data/lib/tina4/public/js/tina4-dev-admin.min.js +1 -1
- data/lib/tina4/public/js/tina4js.min.js +47 -0
- data/lib/tina4/query_builder.rb +374 -0
- data/lib/tina4/queue.rb +219 -61
- data/lib/tina4/queue_backends/lite_backend.rb +42 -7
- data/lib/tina4/queue_backends/mongo_backend.rb +126 -0
- data/lib/tina4/rack_app.rb +200 -11
- data/lib/tina4/request.rb +14 -1
- data/lib/tina4/response.rb +26 -0
- data/lib/tina4/response_cache.rb +446 -29
- data/lib/tina4/router.rb +127 -0
- data/lib/tina4/service_runner.rb +1 -1
- data/lib/tina4/session.rb +6 -1
- data/lib/tina4/session_handlers/database_handler.rb +66 -0
- data/lib/tina4/swagger.rb +1 -1
- data/lib/tina4/templates/errors/404.twig +2 -2
- data/lib/tina4/templates/errors/500.twig +1 -1
- data/lib/tina4/validator.rb +174 -0
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/websocket.rb +23 -4
- data/lib/tina4/websocket_backplane.rb +118 -0
- data/lib/tina4.rb +126 -5
- metadata +40 -3
data/lib/tina4/queue.rb
CHANGED
|
@@ -4,27 +4,42 @@ require "securerandom"
|
|
|
4
4
|
|
|
5
5
|
module Tina4
|
|
6
6
|
class QueueMessage
|
|
7
|
-
attr_reader :id, :topic, :payload, :created_at, :attempts
|
|
7
|
+
attr_reader :id, :topic, :payload, :created_at, :attempts, :priority, :available_at
|
|
8
8
|
attr_accessor :status
|
|
9
9
|
|
|
10
|
-
def initialize(topic:, payload:, id: nil)
|
|
10
|
+
def initialize(topic:, payload:, id: nil, priority: 0, available_at: nil, attempts: 0)
|
|
11
11
|
@id = id || SecureRandom.uuid
|
|
12
12
|
@topic = topic
|
|
13
13
|
@payload = payload
|
|
14
14
|
@created_at = Time.now
|
|
15
|
-
@attempts =
|
|
15
|
+
@attempts = attempts
|
|
16
|
+
@priority = priority
|
|
17
|
+
@available_at = available_at
|
|
16
18
|
@status = :pending
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# Re-queue this message with incremented attempts.
|
|
22
|
+
# Delegates to the queue's backend via the queue reference.
|
|
23
|
+
def retry(queue:, delay_seconds: 0)
|
|
24
|
+
@attempts += 1
|
|
25
|
+
@status = :pending
|
|
26
|
+
@available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
|
|
27
|
+
queue.backend.enqueue(self)
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
19
31
|
def to_hash
|
|
20
|
-
{
|
|
32
|
+
h = {
|
|
21
33
|
id: @id,
|
|
22
34
|
topic: @topic,
|
|
23
35
|
payload: @payload,
|
|
24
36
|
created_at: @created_at.iso8601,
|
|
25
37
|
attempts: @attempts,
|
|
26
|
-
status: @status
|
|
38
|
+
status: @status,
|
|
39
|
+
priority: @priority
|
|
27
40
|
}
|
|
41
|
+
h[:available_at] = @available_at.iso8601 if @available_at
|
|
42
|
+
h
|
|
28
43
|
end
|
|
29
44
|
|
|
30
45
|
def to_json(*_args)
|
|
@@ -34,34 +49,61 @@ module Tina4
|
|
|
34
49
|
def increment_attempts!
|
|
35
50
|
@attempts += 1
|
|
36
51
|
end
|
|
37
|
-
end
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
def
|
|
41
|
-
@
|
|
53
|
+
# Mark this job as completed.
|
|
54
|
+
def complete
|
|
55
|
+
@status = :completed
|
|
42
56
|
end
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@
|
|
47
|
-
|
|
48
|
-
|
|
58
|
+
# Mark this job as failed with a reason.
|
|
59
|
+
def fail(reason = "")
|
|
60
|
+
@status = :failed
|
|
61
|
+
@error = reason
|
|
62
|
+
@attempts += 1
|
|
49
63
|
end
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
|
|
65
|
+
# Reject this job with a reason. Alias for fail().
|
|
66
|
+
def reject(reason = "")
|
|
67
|
+
fail(reason)
|
|
53
68
|
end
|
|
69
|
+
|
|
70
|
+
attr_reader :error
|
|
54
71
|
end
|
|
55
72
|
|
|
56
|
-
# Queue —
|
|
57
|
-
#
|
|
73
|
+
# Queue — unified wrapper for queue management operations.
|
|
74
|
+
# Auto-detects backend from TINA4_QUEUE_BACKEND env var.
|
|
75
|
+
#
|
|
76
|
+
# Usage:
|
|
77
|
+
# # Auto-detect from env (default: lite/file backend)
|
|
78
|
+
# queue = Queue.new(topic: "tasks")
|
|
79
|
+
#
|
|
80
|
+
# # Explicit backend
|
|
81
|
+
# queue = Queue.new(topic: "tasks", backend: :rabbitmq)
|
|
82
|
+
#
|
|
83
|
+
# # Or pass a backend instance directly (legacy)
|
|
84
|
+
# queue = Queue.new(topic: "tasks", backend: my_backend)
|
|
58
85
|
class Queue
|
|
59
86
|
attr_reader :topic, :max_retries
|
|
60
87
|
|
|
61
88
|
def initialize(topic:, backend: nil, max_retries: 3)
|
|
62
89
|
@topic = topic
|
|
63
|
-
@backend = backend || Tina4::QueueBackends::LiteBackend.new
|
|
64
90
|
@max_retries = max_retries
|
|
91
|
+
@backend = resolve_backend_arg(backend)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Push a job onto the queue. Returns the QueueMessage.
|
|
95
|
+
# priority: higher-priority messages are dequeued first (default 0).
|
|
96
|
+
# delay_seconds: delay before the message becomes available (default 0).
|
|
97
|
+
def push(payload, priority: 0, delay_seconds: 0)
|
|
98
|
+
available_at = delay_seconds > 0 ? Time.now + delay_seconds : nil
|
|
99
|
+
message = QueueMessage.new(topic: @topic, payload: payload, priority: priority, available_at: available_at)
|
|
100
|
+
@backend.enqueue(message)
|
|
101
|
+
message
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Pop the next available job. Returns QueueMessage or nil.
|
|
105
|
+
def pop
|
|
106
|
+
@backend.dequeue(@topic)
|
|
65
107
|
end
|
|
66
108
|
|
|
67
109
|
# Get dead letter jobs — messages that exceeded max retries.
|
|
@@ -83,71 +125,187 @@ module Tina4
|
|
|
83
125
|
@backend.retry_failed(@topic, max_retries: @max_retries)
|
|
84
126
|
end
|
|
85
127
|
|
|
86
|
-
#
|
|
87
|
-
def
|
|
88
|
-
|
|
128
|
+
# Produce a message onto a topic. Convenience wrapper around push().
|
|
129
|
+
def produce(topic, payload)
|
|
130
|
+
message = QueueMessage.new(topic: topic, payload: payload)
|
|
131
|
+
@backend.enqueue(message)
|
|
132
|
+
message
|
|
89
133
|
end
|
|
90
|
-
end
|
|
91
134
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
135
|
+
# Consume jobs from a topic using an Enumerator (yield pattern).
|
|
136
|
+
#
|
|
137
|
+
# Usage:
|
|
138
|
+
# queue.consume("emails") do |job|
|
|
139
|
+
# process(job)
|
|
140
|
+
# end
|
|
141
|
+
#
|
|
142
|
+
# # Consume a specific job by ID:
|
|
143
|
+
# queue.consume("emails", id: "abc-123") do |job|
|
|
144
|
+
# process(job)
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# # Or as an enumerator:
|
|
148
|
+
# queue.consume("emails").each { |job| process(job) }
|
|
149
|
+
#
|
|
150
|
+
def consume(topic = nil, id: nil, &block)
|
|
151
|
+
topic ||= @topic
|
|
100
152
|
|
|
101
|
-
|
|
102
|
-
|
|
153
|
+
if block_given?
|
|
154
|
+
if id
|
|
155
|
+
job = pop_by_id(topic, id)
|
|
156
|
+
yield job if job
|
|
157
|
+
else
|
|
158
|
+
while (job = @backend.dequeue(topic))
|
|
159
|
+
yield job
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
# Return an Enumerator when no block given
|
|
164
|
+
Enumerator.new do |yielder|
|
|
165
|
+
if id
|
|
166
|
+
job = pop_by_id(topic, id)
|
|
167
|
+
yielder << job if job
|
|
168
|
+
else
|
|
169
|
+
while (job = @backend.dequeue(topic))
|
|
170
|
+
yielder << job
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
103
175
|
end
|
|
104
176
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
177
|
+
# Pop a specific job by ID from the queue.
|
|
178
|
+
def pop_by_id(topic, id)
|
|
179
|
+
return nil unless @backend.respond_to?(:find_by_id)
|
|
180
|
+
@backend.find_by_id(topic, id)
|
|
181
|
+
end
|
|
108
182
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
183
|
+
# Get the number of messages by status.
|
|
184
|
+
# status: "pending" (default) counts pending messages in the topic queue.
|
|
185
|
+
# status: "failed" or "dead" counts messages in the dead_letter directory.
|
|
186
|
+
def size(status: "pending")
|
|
187
|
+
case status.to_s
|
|
188
|
+
when "pending"
|
|
189
|
+
@backend.size(@topic)
|
|
190
|
+
when "failed", "dead"
|
|
191
|
+
if @backend.respond_to?(:dead_letter_count)
|
|
192
|
+
@backend.dead_letter_count(@topic)
|
|
113
193
|
else
|
|
114
|
-
|
|
194
|
+
0
|
|
115
195
|
end
|
|
196
|
+
else
|
|
197
|
+
@backend.size(@topic)
|
|
116
198
|
end
|
|
117
199
|
end
|
|
118
200
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
201
|
+
# Get the underlying backend instance.
|
|
202
|
+
def backend
|
|
203
|
+
@backend
|
|
122
204
|
end
|
|
123
205
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
206
|
+
# Resolve the default backend from env vars.
|
|
207
|
+
def self.resolve_backend(name = nil)
|
|
208
|
+
chosen = name || ENV.fetch("TINA4_QUEUE_BACKEND", "file").downcase.strip
|
|
209
|
+
|
|
210
|
+
case chosen.to_s
|
|
211
|
+
when "lite", "file", "default"
|
|
212
|
+
Tina4::QueueBackends::LiteBackend.new
|
|
213
|
+
when "rabbitmq"
|
|
214
|
+
config = resolve_rabbitmq_config
|
|
215
|
+
Tina4::QueueBackends::RabbitmqBackend.new(config)
|
|
216
|
+
when "kafka"
|
|
217
|
+
config = resolve_kafka_config
|
|
218
|
+
Tina4::QueueBackends::KafkaBackend.new(config)
|
|
219
|
+
when "mongodb", "mongo"
|
|
220
|
+
config = resolve_mongo_config
|
|
221
|
+
Tina4::QueueBackends::MongoBackend.new(config)
|
|
222
|
+
else
|
|
223
|
+
raise ArgumentError, "Unknown queue backend: #{chosen.inspect}. Use 'lite', 'rabbitmq', 'kafka', or 'mongodb'."
|
|
224
|
+
end
|
|
127
225
|
end
|
|
128
226
|
|
|
129
227
|
private
|
|
130
228
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
|
|
229
|
+
def resolve_backend_arg(backend)
|
|
230
|
+
# If a backend instance is passed directly (legacy), use it
|
|
231
|
+
return backend if backend && !backend.is_a?(Symbol) && !backend.is_a?(String)
|
|
232
|
+
# If a symbol or string name is passed, resolve it
|
|
233
|
+
Queue.resolve_backend(backend)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def self.resolve_rabbitmq_config
|
|
237
|
+
config = {}
|
|
238
|
+
url = ENV["TINA4_QUEUE_URL"]
|
|
239
|
+
if url
|
|
240
|
+
config = parse_amqp_url(url)
|
|
241
|
+
end
|
|
242
|
+
config[:host] ||= ENV.fetch("TINA4_RABBITMQ_HOST", "localhost")
|
|
243
|
+
config[:port] ||= (ENV["TINA4_RABBITMQ_PORT"] || 5672).to_i
|
|
244
|
+
config[:username] ||= ENV.fetch("TINA4_RABBITMQ_USERNAME", "guest")
|
|
245
|
+
config[:password] ||= ENV.fetch("TINA4_RABBITMQ_PASSWORD", "guest")
|
|
246
|
+
config[:vhost] ||= ENV.fetch("TINA4_RABBITMQ_VHOST", "/")
|
|
247
|
+
config
|
|
248
|
+
end
|
|
134
249
|
|
|
135
|
-
|
|
136
|
-
|
|
250
|
+
def self.resolve_kafka_config
|
|
251
|
+
config = {}
|
|
252
|
+
url = ENV["TINA4_QUEUE_URL"]
|
|
253
|
+
if url
|
|
254
|
+
config[:brokers] = url.sub("kafka://", "")
|
|
137
255
|
end
|
|
256
|
+
brokers = ENV["TINA4_KAFKA_BROKERS"]
|
|
257
|
+
config[:brokers] = brokers if brokers
|
|
258
|
+
config[:brokers] ||= "localhost:9092"
|
|
259
|
+
config[:group_id] = ENV.fetch("TINA4_KAFKA_GROUP_ID", "tina4_consumer_group")
|
|
260
|
+
config
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.resolve_mongo_config
|
|
264
|
+
config = {}
|
|
265
|
+
uri = ENV["TINA4_MONGO_URI"]
|
|
266
|
+
config[:uri] = uri if uri
|
|
267
|
+
config[:host] = ENV.fetch("TINA4_MONGO_HOST", "localhost") unless uri
|
|
268
|
+
config[:port] = (ENV["TINA4_MONGO_PORT"] || 27017).to_i unless uri
|
|
269
|
+
username = ENV["TINA4_MONGO_USERNAME"]
|
|
270
|
+
password = ENV["TINA4_MONGO_PASSWORD"]
|
|
271
|
+
config[:username] = username if username
|
|
272
|
+
config[:password] = password if password
|
|
273
|
+
config[:db] = ENV.fetch("TINA4_MONGO_DB", "tina4")
|
|
274
|
+
config[:collection] = ENV.fetch("TINA4_MONGO_COLLECTION", "tina4_queue")
|
|
275
|
+
config
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def self.parse_amqp_url(url)
|
|
279
|
+
config = {}
|
|
280
|
+
url = url.sub("amqp://", "").sub("amqps://", "")
|
|
138
281
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
282
|
+
if url.include?("@")
|
|
283
|
+
creds, rest = url.split("@", 2)
|
|
284
|
+
if creds.include?(":")
|
|
285
|
+
config[:username], config[:password] = creds.split(":", 2)
|
|
286
|
+
else
|
|
287
|
+
config[:username] = creds
|
|
288
|
+
end
|
|
289
|
+
else
|
|
290
|
+
rest = url
|
|
291
|
+
end
|
|
144
292
|
|
|
145
|
-
if
|
|
146
|
-
|
|
147
|
-
|
|
293
|
+
if rest.include?("/")
|
|
294
|
+
hostport, vhost = rest.split("/", 2)
|
|
295
|
+
config[:vhost] = vhost.start_with?("/") ? vhost : "/#{vhost}" if vhost && !vhost.empty?
|
|
148
296
|
else
|
|
149
|
-
|
|
297
|
+
hostport = rest
|
|
150
298
|
end
|
|
299
|
+
|
|
300
|
+
if hostport.include?(":")
|
|
301
|
+
host, port = hostport.split(":", 2)
|
|
302
|
+
config[:host] = host
|
|
303
|
+
config[:port] = port.to_i
|
|
304
|
+
elsif hostport && !hostport.empty?
|
|
305
|
+
config[:host] = hostport
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
config
|
|
151
309
|
end
|
|
152
310
|
end
|
|
153
311
|
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
require "json"
|
|
3
3
|
require "fileutils"
|
|
4
|
+
require "time"
|
|
4
5
|
|
|
5
6
|
module Tina4
|
|
6
7
|
module QueueBackends
|
|
@@ -27,17 +28,37 @@ module Tina4
|
|
|
27
28
|
dir = topic_path(topic)
|
|
28
29
|
return nil unless Dir.exist?(dir)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
now = Time.now
|
|
32
|
+
candidates = []
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
Dir.glob(File.join(dir, "*.json")).each do |f|
|
|
35
|
+
data = JSON.parse(File.read(f))
|
|
36
|
+
# Skip messages that are not yet available (delayed)
|
|
37
|
+
if data["available_at"]
|
|
38
|
+
available_at = Time.parse(data["available_at"])
|
|
39
|
+
next if available_at > now
|
|
40
|
+
end
|
|
41
|
+
candidates << { file: f, data: data, priority: data["priority"] || 0, mtime: File.mtime(f) }
|
|
42
|
+
rescue JSON::ParserError
|
|
43
|
+
next
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
return nil if candidates.empty?
|
|
47
|
+
|
|
48
|
+
# Sort by priority descending (higher first), then by mtime ascending (oldest first)
|
|
49
|
+
candidates.sort_by! { |c| [-c[:priority], c[:mtime]] }
|
|
50
|
+
|
|
51
|
+
chosen = candidates.first
|
|
52
|
+
File.delete(chosen[:file])
|
|
53
|
+
data = chosen[:data]
|
|
36
54
|
|
|
37
55
|
Tina4::QueueMessage.new(
|
|
38
|
-
topic: data["topic"],
|
|
56
|
+
topic: data["topic"] || topic.to_s,
|
|
39
57
|
payload: data["payload"],
|
|
40
|
-
id: data["id"]
|
|
58
|
+
id: data["id"],
|
|
59
|
+
priority: data["priority"] || 0,
|
|
60
|
+
available_at: data["available_at"] ? Time.parse(data["available_at"]) : nil,
|
|
61
|
+
attempts: data["attempts"] || 0
|
|
41
62
|
)
|
|
42
63
|
end
|
|
43
64
|
end
|
|
@@ -61,6 +82,20 @@ module Tina4
|
|
|
61
82
|
Dir.glob(File.join(dir, "*.json")).length
|
|
62
83
|
end
|
|
63
84
|
|
|
85
|
+
# Count dead-letter / failed messages for a topic.
|
|
86
|
+
def dead_letter_count(topic)
|
|
87
|
+
return 0 unless Dir.exist?(@dead_letter_dir)
|
|
88
|
+
|
|
89
|
+
count = 0
|
|
90
|
+
Dir.glob(File.join(@dead_letter_dir, "*.json")).each do |file|
|
|
91
|
+
data = JSON.parse(File.read(file))
|
|
92
|
+
count += 1 if data["topic"] == topic.to_s
|
|
93
|
+
rescue JSON::ParserError
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
count
|
|
97
|
+
end
|
|
98
|
+
|
|
64
99
|
def topics
|
|
65
100
|
return [] unless Dir.exist?(@dir)
|
|
66
101
|
Dir.children(@dir)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tina4
|
|
4
|
+
module QueueBackends
|
|
5
|
+
class MongoBackend
|
|
6
|
+
def initialize(options = {})
|
|
7
|
+
require "mongo"
|
|
8
|
+
|
|
9
|
+
uri = options[:uri] || ENV["TINA4_MONGO_URI"]
|
|
10
|
+
host = options[:host] || ENV.fetch("TINA4_MONGO_HOST", "localhost")
|
|
11
|
+
port = (options[:port] || ENV.fetch("TINA4_MONGO_PORT", 27017)).to_i
|
|
12
|
+
username = options[:username] || ENV["TINA4_MONGO_USERNAME"]
|
|
13
|
+
password = options[:password] || ENV["TINA4_MONGO_PASSWORD"]
|
|
14
|
+
db_name = options[:db] || ENV.fetch("TINA4_MONGO_DB", "tina4")
|
|
15
|
+
@collection_name = options[:collection] || ENV.fetch("TINA4_MONGO_COLLECTION", "tina4_queue")
|
|
16
|
+
|
|
17
|
+
if uri
|
|
18
|
+
@client = Mongo::Client.new(uri)
|
|
19
|
+
else
|
|
20
|
+
conn_options = { database: db_name }
|
|
21
|
+
conn_options[:user] = username if username
|
|
22
|
+
conn_options[:password] = password if password
|
|
23
|
+
@client = Mongo::Client.new(["#{host}:#{port}"], conn_options)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@db = @client.database
|
|
27
|
+
create_indexes
|
|
28
|
+
rescue LoadError
|
|
29
|
+
raise "MongoDB backend requires the 'mongo' gem. Install with: gem install mongo"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def enqueue(message)
|
|
33
|
+
collection.insert_one(
|
|
34
|
+
_id: message.id,
|
|
35
|
+
topic: message.topic,
|
|
36
|
+
payload: message.payload,
|
|
37
|
+
created_at: message.created_at.utc,
|
|
38
|
+
attempts: message.attempts,
|
|
39
|
+
status: "pending"
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dequeue(topic)
|
|
44
|
+
doc = collection.find_one_and_update(
|
|
45
|
+
{ topic: topic, status: "pending" },
|
|
46
|
+
{ "$set" => { status: "processing" } },
|
|
47
|
+
sort: { created_at: 1 },
|
|
48
|
+
return_document: :after
|
|
49
|
+
)
|
|
50
|
+
return nil unless doc
|
|
51
|
+
|
|
52
|
+
Tina4::QueueMessage.new(
|
|
53
|
+
topic: doc["topic"],
|
|
54
|
+
payload: doc["payload"],
|
|
55
|
+
id: doc["_id"]
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def acknowledge(message)
|
|
60
|
+
collection.delete_one(_id: message.id)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def requeue(message)
|
|
64
|
+
collection.find_one_and_update(
|
|
65
|
+
{ _id: message.id },
|
|
66
|
+
{ "$set" => { status: "pending" }, "$inc" => { attempts: 1 } },
|
|
67
|
+
upsert: true
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def dead_letter(message)
|
|
72
|
+
collection.find_one_and_update(
|
|
73
|
+
{ _id: message.id },
|
|
74
|
+
{ "$set" => { status: "dead", topic: "#{message.topic}.dead_letter" } },
|
|
75
|
+
upsert: true
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def size(topic)
|
|
80
|
+
collection.count_documents(topic: topic, status: "pending")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def dead_letters(topic, max_retries: 3)
|
|
84
|
+
collection.find(topic: "#{topic}.dead_letter", status: "dead").map do |doc|
|
|
85
|
+
Tina4::QueueMessage.new(
|
|
86
|
+
topic: doc["topic"],
|
|
87
|
+
payload: doc["payload"],
|
|
88
|
+
id: doc["_id"]
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def purge(topic, status)
|
|
94
|
+
result = collection.delete_many(topic: topic, status: status.to_s)
|
|
95
|
+
result.deleted_count
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def retry_failed(topic, max_retries: 3)
|
|
99
|
+
result = collection.update_many(
|
|
100
|
+
{ topic: topic, status: "failed", attempts: { "$lt" => max_retries } },
|
|
101
|
+
{ "$set" => { status: "pending" } }
|
|
102
|
+
)
|
|
103
|
+
result.modified_count
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def close
|
|
107
|
+
@client&.close
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def collection
|
|
113
|
+
@db[@collection_name]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def create_indexes
|
|
117
|
+
collection.indexes.create_many([
|
|
118
|
+
{ key: { topic: 1, status: 1, created_at: 1 } },
|
|
119
|
+
{ key: { topic: 1, status: 1, attempts: 1 } }
|
|
120
|
+
])
|
|
121
|
+
rescue Mongo::Error => e
|
|
122
|
+
Tina4::Log.warning("MongoDB index creation failed: #{e.message}")
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|