tina4ruby 3.2.1 → 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 +19 -20
- data/lib/tina4/auth.rb +137 -27
- data/lib/tina4/auto_crud.rb +55 -3
- data/lib/tina4/cli.rb +75 -2
- data/lib/tina4/cors.rb +1 -1
- data/lib/tina4/database.rb +131 -28
- data/lib/tina4/database_result.rb +122 -8
- data/lib/tina4/env.rb +1 -1
- data/lib/tina4/frond.rb +148 -2
- data/lib/tina4/localization.rb +1 -1
- data/lib/tina4/middleware.rb +349 -1
- data/lib/tina4/migration.rb +132 -11
- data/lib/tina4/orm.rb +17 -8
- 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 +128 -90
- 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 +194 -18
- data/lib/tina4/request.rb +14 -1
- data/lib/tina4/response.rb +26 -0
- 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/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 +64 -4
- metadata +12 -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,28 +49,25 @@ 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
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
def push(topic, payload)
|
|
57
|
-
publish(topic, payload)
|
|
58
|
-
end
|
|
70
|
+
attr_reader :error
|
|
59
71
|
end
|
|
60
72
|
|
|
61
73
|
# Queue — unified wrapper for queue management operations.
|
|
@@ -80,8 +92,11 @@ module Tina4
|
|
|
80
92
|
end
|
|
81
93
|
|
|
82
94
|
# Push a job onto the queue. Returns the QueueMessage.
|
|
83
|
-
|
|
84
|
-
|
|
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)
|
|
85
100
|
@backend.enqueue(message)
|
|
86
101
|
message
|
|
87
102
|
end
|
|
@@ -110,9 +125,77 @@ module Tina4
|
|
|
110
125
|
@backend.retry_failed(@topic, max_retries: @max_retries)
|
|
111
126
|
end
|
|
112
127
|
|
|
113
|
-
#
|
|
114
|
-
def
|
|
115
|
-
|
|
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
|
|
133
|
+
end
|
|
134
|
+
|
|
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
|
|
152
|
+
|
|
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
|
|
175
|
+
end
|
|
176
|
+
|
|
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
|
|
182
|
+
|
|
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)
|
|
193
|
+
else
|
|
194
|
+
0
|
|
195
|
+
end
|
|
196
|
+
else
|
|
197
|
+
@backend.size(@topic)
|
|
198
|
+
end
|
|
116
199
|
end
|
|
117
200
|
|
|
118
201
|
# Get the underlying backend instance.
|
|
@@ -121,9 +204,8 @@ module Tina4
|
|
|
121
204
|
end
|
|
122
205
|
|
|
123
206
|
# Resolve the default backend from env vars.
|
|
124
|
-
# Class method so Producer can also use it.
|
|
125
207
|
def self.resolve_backend(name = nil)
|
|
126
|
-
chosen = name || ENV.fetch("TINA4_QUEUE_BACKEND", "
|
|
208
|
+
chosen = name || ENV.fetch("TINA4_QUEUE_BACKEND", "file").downcase.strip
|
|
127
209
|
|
|
128
210
|
case chosen.to_s
|
|
129
211
|
when "lite", "file", "default"
|
|
@@ -134,8 +216,11 @@ module Tina4
|
|
|
134
216
|
when "kafka"
|
|
135
217
|
config = resolve_kafka_config
|
|
136
218
|
Tina4::QueueBackends::KafkaBackend.new(config)
|
|
219
|
+
when "mongodb", "mongo"
|
|
220
|
+
config = resolve_mongo_config
|
|
221
|
+
Tina4::QueueBackends::MongoBackend.new(config)
|
|
137
222
|
else
|
|
138
|
-
raise ArgumentError, "Unknown queue backend: #{chosen.inspect}. Use 'lite', 'rabbitmq', or '
|
|
223
|
+
raise ArgumentError, "Unknown queue backend: #{chosen.inspect}. Use 'lite', 'rabbitmq', 'kafka', or 'mongodb'."
|
|
139
224
|
end
|
|
140
225
|
end
|
|
141
226
|
|
|
@@ -175,6 +260,21 @@ module Tina4
|
|
|
175
260
|
config
|
|
176
261
|
end
|
|
177
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
|
+
|
|
178
278
|
def self.parse_amqp_url(url)
|
|
179
279
|
config = {}
|
|
180
280
|
url = url.sub("amqp://", "").sub("amqps://", "")
|
|
@@ -208,66 +308,4 @@ module Tina4
|
|
|
208
308
|
config
|
|
209
309
|
end
|
|
210
310
|
end
|
|
211
|
-
|
|
212
|
-
class Consumer
|
|
213
|
-
def initialize(topic:, backend: nil, max_retries: 3)
|
|
214
|
-
@topic = topic
|
|
215
|
-
@backend = backend || Tina4::Queue.resolve_backend
|
|
216
|
-
@max_retries = max_retries
|
|
217
|
-
@handlers = []
|
|
218
|
-
@running = false
|
|
219
|
-
end
|
|
220
|
-
|
|
221
|
-
def on_message(&block)
|
|
222
|
-
@handlers << block
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def start(poll_interval: 1)
|
|
226
|
-
@running = true
|
|
227
|
-
Tina4::Log.info("Consumer started for topic: #{@topic}")
|
|
228
|
-
|
|
229
|
-
while @running
|
|
230
|
-
message = @backend.dequeue(@topic)
|
|
231
|
-
if message
|
|
232
|
-
process_message(message)
|
|
233
|
-
else
|
|
234
|
-
sleep(poll_interval)
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def stop
|
|
240
|
-
@running = false
|
|
241
|
-
Tina4::Log.info("Consumer stopped for topic: #{@topic}")
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def process_one
|
|
245
|
-
message = @backend.dequeue(@topic)
|
|
246
|
-
process_message(message) if message
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
private
|
|
250
|
-
|
|
251
|
-
def process_message(message)
|
|
252
|
-
message.increment_attempts!
|
|
253
|
-
message.status = :processing
|
|
254
|
-
|
|
255
|
-
@handlers.each do |handler|
|
|
256
|
-
handler.call(message)
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
message.status = :completed
|
|
260
|
-
@backend.acknowledge(message)
|
|
261
|
-
rescue => e
|
|
262
|
-
Tina4::Log.error("Queue message failed: #{message.id} - #{e.message}")
|
|
263
|
-
message.status = :failed
|
|
264
|
-
|
|
265
|
-
if message.attempts < @max_retries
|
|
266
|
-
message.status = :pending
|
|
267
|
-
@backend.requeue(message)
|
|
268
|
-
else
|
|
269
|
-
@backend.dead_letter(message)
|
|
270
|
-
end
|
|
271
|
-
end
|
|
272
|
-
end
|
|
273
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
|