tina4ruby 3.13.104 → 3.13.105

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: b2e628488f2b0de1d7ec0d271cc07562ff66aa22c198adfea5fb2d9f61581987
4
- data.tar.gz: 1416a35dac038e0a20befbec32f68d5e2cdd4d160ec440c1c18d40c38a3b2034
3
+ metadata.gz: 4ab6d7ce15da93e814204665a8690be0cc2ac58dc1152fa75f050b91fcb0c4f0
4
+ data.tar.gz: 35fc27eb9d71c17a0049d983d34ed6c1584e014338192e5ace087e5e2bec0746
5
5
  SHA512:
6
- metadata.gz: 118eb151f7ceab9069f13f2e147a81c9e24468fb97d850acddb0054b15acd6e922369c59a83127a09e09605cf8460b0cc606268911325e63651bbda64c315b36
7
- data.tar.gz: 6045f620f3e0b1a6f489cd9215b1c8d52dba5ea3eb4f6db15ee768963a763ac6deb51f84e1809ea10de660cd89ad035a8cca06779f3373796ebf455b324631a2
6
+ metadata.gz: f2e66595ddcf7eca7d498428149ca38a83caf4871d8d5e29e31ef9d72f7f38f2ed9c3afe638ba2891bf9ffda5b00dbbf54883ddc704bf06787f9f06060b0603b
7
+ data.tar.gz: c45ab7892609d2be7e0b3dc432a8d54bef9dd8281e3594198b9fa93e9170ceeb60372624c69d49978884ef596cb7ff35eeb720216c4d240a176e372ffa33d803
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ number means the same thing everywhere.
6
6
  **The authoritative release notes for every shipped version live in the documentation:**
7
7
  https://tina4.com/ruby/36-releases
8
8
 
9
+ ## 3.13.105
10
+
11
+ Bug release. Route inspection stops touching the app; Firebird's migration
12
+ ledger tolerates whatever case the driver hands back; PHP loses a colon-in-
13
+ filename that broke Windows checkouts.
14
+
15
+ ### Route inspection scans, never boots
16
+
17
+ - `tina4 routes` now walks canonical route files and never executes the
18
+ application entrypoint or starts the server. Feature 115 / ADR-0058.
19
+ - Fixes the case where `tina4 routes --override` would boot the app on the
20
+ same port and kill whatever process was already holding it (tina4-python
21
+ issue #104).
22
+
23
+ ### Firebird migration ledger is case-agnostic
24
+
25
+ - `tina4_migration` reads and writes work regardless of the case the
26
+ Firebird driver returns for the `migration_name` column.
27
+ - Uses the atomic sequence table pattern already in place for other engines.
28
+
9
29
  ## 3.13.103
10
30
 
11
31
  ### Metrics reports what it can prove
data/lib/tina4/cli.rb CHANGED
@@ -1013,8 +1013,8 @@ module Tina4
1013
1013
 
1014
1014
  def cmd_routes(_argv = nil)
1015
1015
  require_relative "../tina4"
1016
- Tina4.initialize!(Dir.pwd)
1017
- load_routes(Dir.pwd)
1016
+ Tina4::Env.load_env(Dir.pwd)
1017
+ load_routes(Dir.pwd, entrypoints: false)
1018
1018
 
1019
1019
  puts "\nRegistered Routes:"
1020
1020
  puts "-" * 60
@@ -2959,7 +2959,7 @@ module Tina4
2959
2959
 
2960
2960
  # ── shared helpers ────────────────────────────────────────────────────
2961
2961
 
2962
- def load_routes(root_dir)
2962
+ def load_routes(root_dir, entrypoints: true)
2963
2963
  route_dirs = %w[src/routes routes src/api api src/orm orm]
2964
2964
  route_dirs.each do |dir|
2965
2965
  route_dir = File.join(root_dir, dir)
@@ -2967,7 +2967,10 @@ module Tina4
2967
2967
  Dir.glob(File.join(route_dir, "**/*.rb")).sort.each { |f| load f }
2968
2968
  end
2969
2969
 
2970
- # Also load app.rb if it exists
2970
+ return unless entrypoints
2971
+
2972
+ # Runtime and console boot load the application entrypoint. Inspection
2973
+ # commands opt out so they can never start a server or take over a port.
2971
2974
  app_file = File.join(root_dir, "app.rb")
2972
2975
  load app_file if File.exist?(app_file)
2973
2976
 
data/lib/tina4/orm.rb CHANGED
@@ -480,13 +480,25 @@ module Tina4
480
480
 
481
481
  # Invalidate every cached query that touches this model's table.
482
482
  #
483
- # Tag-scoped, NOT a wholesale flush: a cached JOIN on another model that
484
- # reads this table is busted too (it carries this table's tag), while a
485
- # query that never touches this table is left intact. Called after every
486
- # ORM write (save/delete/force_delete/restore) so a read-after-write never
487
- # serves a stale/deleted row (CACHE-DEC-01).
483
+ # Tag-scoped in the ORM layer (a cached JOIN on another model that reads
484
+ # this table is busted too because it carries this table's tag; a query
485
+ # that never touches this table is left intact), then cascaded to the
486
+ # DB layer on this model's bound connection so an out-of-band write /
487
+ # deliberate refresh / race-with-another-process cannot leave stale rows
488
+ # in db.fetch's persistent cache. Called after every ORM write
489
+ # (save/delete/force_delete/restore) so a read-after-write never serves
490
+ # a stale/deleted row (CACHE-DEC-01). 3.13.105 (parity port of PY-06-22)
491
+ # added the DB-layer cascade — previously the two cache layers disagreed
492
+ # under TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
488
493
  def clear_cache
489
494
  query_cache.clear_tag(table_name.to_s.downcase)
495
+ begin
496
+ get_db.cache_clear
497
+ rescue StandardError
498
+ # A resolvable DB is not guaranteed at every clear_cache call site
499
+ # (module-import time in odd bootstraps, tests that mutate bindings);
500
+ # never let a cache-clear crash a save/delete.
501
+ end
490
502
  nil
491
503
  end
492
504
 
data/lib/tina4/queue.rb CHANGED
@@ -171,7 +171,14 @@ module Tina4
171
171
  @backend.close
172
172
  end
173
173
 
174
- # Get jobs that failed but are still eligible for retry (under max_retries).
174
+ # Get jobs that failed at least once but are still being retried
175
+ # (0 < attempts < max_retries). These live in the pending queue under
176
+ # the auto-retry lifecycle (fail() re-queues them with an incremented
177
+ # attempts count and a retry_backoff delay) so pop() picks them up
178
+ # again. They are NOT counted by size("failed") — that alias counts
179
+ # the dead-letter store, matching dead_letters(). To include retryable-
180
+ # failed jobs in a total, use size("pending"). Terminal failures are
181
+ # returned by dead_letters().
175
182
  def failed # -> list[dict]
176
183
  refuse_operation!("failed()") unless @backend.respond_to?(:failed)
177
184
  @backend.failed(@topic, max_retries: @max_retries)
@@ -179,13 +186,24 @@ module Tina4
179
186
 
180
187
  # Retry a specific failed job by ID, or all dead-letter jobs if no id given.
181
188
  # Returns true if re-queued.
189
+ #
190
+ # The no-arg branch is materialised by the backend (LiteBackend#retry_job
191
+ # iterates every dead-letter file with .each, MongoBackend materialises
192
+ # find(...).to_a before iterating) — no generator inside any() /
193
+ # short-circuit reduce can silently leave dead letters behind (parity port
194
+ # of PY-12-04, 3.13.105).
182
195
  def retry(job_id = nil, delay_seconds: 0) # -> bool
183
196
  refuse_operation!("retry()") unless @backend.respond_to?(:retry_job)
184
197
  @backend.retry_job(@topic, job_id: job_id, delay_seconds: delay_seconds)
185
198
  end
186
199
 
187
- # Get dead letter jobs — messages that exceeded max retries.
188
- # Pass max_retries to override the queue's default.
200
+ # Get jobs that exceeded max_retries — terminal failures.
201
+ #
202
+ # Same set counted by size("failed") / size("dead") / size("dead_letter")
203
+ # (three aliases for the dead-letter store). To LIST retryable-but-
204
+ # attempted jobs (attempts > 0 AND attempts < max_retries) that are still
205
+ # being auto-retried, use failed() — those live in the pending queue and
206
+ # are NOT dead letters. Pass max_retries to override the queue's default.
189
207
  def dead_letters(max_retries: nil) # -> list[dict]
190
208
  refuse_operation!("dead_letters()") unless @backend.respond_to?(:dead_letters)
191
209
  @backend.dead_letters(@topic, max_retries: max_retries || @max_retries)
@@ -328,9 +346,21 @@ module Tina4
328
346
  attach(@backend.find_by_id(topic || @topic, id))
329
347
  end
330
348
 
331
- # Get the number of messages by status.
332
- # status: "pending" (default) counts pending messages in the topic queue.
333
- # status: "failed" or "dead" counts messages in the dead_letter directory.
349
+ # Count jobs by status.
350
+ #
351
+ # "pending" counts jobs waiting to be popped INCLUDES retryable-but-
352
+ # attempted ones, because they live in the pending queue under the
353
+ # auto-retry lifecycle (see failed()).
354
+ # "reserved" counts jobs a consumer has popped but not yet
355
+ # completed/failed (in-flight against the visibility timeout).
356
+ # "completed" counts jobs the consumer has finished successfully
357
+ # (0 on the lite/file backend which deletes on complete; backends that
358
+ # track completion expose completed_count).
359
+ # "failed", "dead", "dead_letter" are ALIASES that all count the
360
+ # dead-letter store — jobs whose attempts >= max_retries and that have
361
+ # given up. Use dead_letters() to list them. Retryable-but-attempted
362
+ # jobs are NOT counted by size("failed"); use failed() to list them or
363
+ # size("pending") to include them in a total.
334
364
  def size(status: "pending")
335
365
  case status.to_s
336
366
  when "pending"
@@ -164,9 +164,26 @@ module Tina4
164
164
 
165
165
  # Explicit re-queue requested by the caller (job.retry()). Always
166
166
  # re-enqueues regardless of the retry limit — a manual override, distinct
167
- # from the automatic fail() path. Increments attempts, clears the error.
167
+ # from the automatic fail() path. Cleans up BOTH the reservation record
168
+ # and any dead-letter file for this id, so a caller who iterates
169
+ # dead_letters and calls job.retry on each doesn't leave the dead-letter
170
+ # directory carrying duplicates (3.13.105, parity port of PY-12-05).
171
+ # Aligns with retry_job(id) which had always unlinked the dead-letter
172
+ # file — two spellings of the same intent that previously diverged.
173
+ # Increments attempts, clears the error.
168
174
  def retry(job, delay_seconds: 0)
169
175
  clear_reservation(job.topic, job.id)
176
+ # Drop any dead-letter file for this id BEFORE the re-queue: if this
177
+ # job came from dead_letters it lives in @dead_letter_dir and would
178
+ # otherwise stay on disk while a fresh pending file appears in
179
+ # topic_path, so the next dead_letters call reports the job again
180
+ # and a consumer processes it twice.
181
+ begin
182
+ File.unlink(job_file(@dead_letter_dir, job.id))
183
+ rescue Errno::ENOENT
184
+ # No dead-letter file for this id (manual .retry on a live job) —
185
+ # nothing to clean up.
186
+ end
170
187
  job.attempts += 1
171
188
  requeue_job(job, delay_seconds: delay_seconds, error: nil)
172
189
  end
@@ -291,8 +291,19 @@ module Tina4
291
291
  .map { |doc| job_from_doc(doc) }
292
292
  end
293
293
 
294
+ # Delete every doc in this queue whose status matches.
295
+ #
296
+ # 3.13.105 (parity port): route the dead-letter statuses ("dead",
297
+ # "failed", "dead_letter") to the ".dead_letter" topic namespace where
298
+ # they actually live — pre-fix, purge("dead") delete_many'd on
299
+ # {topic: topic, status: "dead"}, matching zero docs and silently
300
+ # returning 0 while the dead letters sat untouched in the sibling
301
+ # namespace. Every path scopes by status so a purge never nukes docs it
302
+ # was not asked to remove, and every path returns deleted_count so a
303
+ # caller can log/assert what was purged.
294
304
  def purge(topic, status)
295
- result = collection.delete_many(topic: topic, status: status.to_s)
305
+ namespace = dead_status?(status) ? "#{topic}.dead_letter" : topic
306
+ result = collection.delete_many(topic: namespace, status: status.to_s)
296
307
  result.deleted_count
297
308
  end
298
309
 
@@ -326,20 +337,31 @@ module Tina4
326
337
  result.modified_count
327
338
  end
328
339
 
329
- # Move ONE dead-lettered job back to its main topic as pending.
330
- # Returns true when a job was revived, false when the id was not found.
340
+ # Move dead-lettered jobs back to their main topic as pending.
341
+ #
342
+ # With +job_id+, revives that ONE dead letter and returns true when
343
+ # found (false otherwise). With no id, iterates every dead letter for
344
+ # the topic and revives ALL — materialised into an array before
345
+ # reducing, so a truthy first result never short-circuits away the
346
+ # rest (the class of bug the parity port of PY-12-04 pins). Ruby's
347
+ # dead-letter model stores the SAME document with topic flipped to
348
+ # "<topic>.dead_letter", so reviving is the reverse in-place update:
349
+ # flip the topic back, reset status/available_at/error. Mirrors
350
+ # LiteBackend#retry_job.
331
351
  def retry_job(topic, job_id: nil, delay_seconds: 0)
332
352
  filter = { topic: "#{topic}.dead_letter", status: "dead" }
333
353
  filter[:_id] = job_id if job_id
334
- doc = collection.find(filter).first
335
- return false unless doc
354
+ docs = collection.find(filter).to_a
355
+ return false if docs.empty?
336
356
 
337
357
  available = delay_seconds.to_f > 0 ? (Time.now.utc + delay_seconds.to_f) : Time.now.utc
338
- collection.update_one(
339
- { _id: doc["_id"] },
340
- { "$set" => { topic: topic, status: "pending", error: nil,
341
- reserved_at: nil, available_at: available } }
342
- )
358
+ docs.each do |doc|
359
+ collection.update_one(
360
+ { _id: doc["_id"] },
361
+ { "$set" => { topic: topic, status: "pending", error: nil,
362
+ reserved_at: nil, available_at: available } }
363
+ )
364
+ end
343
365
  true
344
366
  end
345
367
 
@@ -358,6 +380,12 @@ module Tina4
358
380
 
359
381
  private
360
382
 
383
+ DEAD_STATES = %w[failed dead dead_letter].freeze
384
+
385
+ def dead_status?(status)
386
+ DEAD_STATES.include?(status.to_s)
387
+ end
388
+
361
389
  # A stored document as a plain Hash with string keys, CARRYING attempts
362
390
  # and error.
363
391
  #
data/lib/tina4/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Tina4
4
- VERSION = "3.13.104"
4
+ VERSION = "3.13.105"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tina4ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.13.104
4
+ version: 3.13.105
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tina4 Team
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-17 00:00:00.000000000 Z
11
+ date: 2026-08-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack