nondisposable 0.6.0 → 0.6.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: 9d0c0a8ad776c00c83c1e8b1e3001c652c5e5b414bb8f79216d3544192843e42
4
- data.tar.gz: f2ab66119b84f495ce62d2104ab1c05701441796a969ae793e0fe033c69e6447
3
+ metadata.gz: 96ee85d55e05d52d472cb9a4b9897e318499282c35818579621daec1bcd7bf9e
4
+ data.tar.gz: f633e949328cb2972dfdff7c44f33072b29dae4b5bd95ab116af24ec504f676c
5
5
  SHA512:
6
- metadata.gz: 5d2c0d50d9296c92465713fcdf8c97ddfcb3f9b4536ff299b80687f4d0ea12c877a718511bd340d3f1bba897c3e1cc38bdd65ee10c9a3c6f37272cffd3a376cb
7
- data.tar.gz: bd6983052fe4a8980a041c99f8aa7a4c41b35c947b5de246d39b10cdbf20c2ad36b2114dd1dad6b481388bcff02a4712977d73bf90c7d132bd0cbd594a5dc245
6
+ metadata.gz: 71c6fbbfcac0534c320decc1cee69147497984a9dc12f19569a2231eacc192a5a2cc328e88decdc5e8f692384bf8ae39705a9c77ff134fb5cccc02fc455c133d
7
+ data.tar.gz: 8abdd8d29e8bfeb5f51da7cc98a6b213a8af428b561a8be6a9d2e84bce470f96a5e2c5bbeeff2f9a4e0dda0d719f5e696562c97b9643278a5d92a5c90ae8ff06
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.6.1] - 2026-10-17
2
+
3
+ ### Fixed
4
+
5
+ - **A populated table is no longer replaced with the bundled list by an update** (an explicit `rollback` still can; see below). In 0.6.0, when a PRIMARY source with a bundled list (the default `disposable-email-domains`) had never been fetched successfully, `DomainListUpdater` put the list bundled with the gem in its place and published it, even over a populated table. **Who is affected:** any host with the `nondisposable_source_snapshots` table whose domain table was populated before the default source had an accepted snapshot of its own. Typically that means an upgrade from 0.4/0.5, with `Sources::DEFAULT` or a multi-source configuration such as `Sources::RECOMMENDED`. If the default source failed on such a host's first update, the table was replaced by the bundled snapshot (plus any other primary that succeeded), and every domain added upstream since the gem's release stopped being blocked until the next successful run. `update` returned false, but the list had already shrunk. Now the bundled stand-in is used **only on an empty table** (a fresh install). With a populated table, publishing is skipped with an error log and the table is kept as it is, until every primary has an accepted snapshot of its own. **The deliberate exception is `DomainListUpdater.rollback`:** withdrawing a poisoned list must always succeed, so there the bundled list still stands in for a primary without a snapshot, whatever the table holds. The emptiness check also bypasses the query cache, so a stale cached answer can never let the bundled list replace rows it never saw. Reported by a codex review in CarHey.
6
+
1
7
  ## [0.6.0] - 2026-10-17
2
8
 
3
9
  Ships everything listed under 0.5.0 too: 0.5.0 was never released on its own.
data/README.md CHANGED
@@ -408,7 +408,7 @@ Each source is fetched into its own row in `nondisposable_source_snapshots` (sta
408
408
  - it is less than half the source's high-water mark, the largest size ever accepted for it (kept on its newest good snapshot, so neither pruning nor a series of 49% cuts can wear it down; `reset_baseline` is the human override), or more than three times its last good one;
409
409
  - it is identical to a snapshot a human rolled back.
410
410
 
411
- Every size is a count of *storable* domains (not junk, not a public suffix, not a known real provider, not a privacy relay), counted the same way whatever your settings, so a payload padded with such lines cannot pass for a full list and changing `block_privacy_relays` or `excluded_domains` never trips a guard. The list is never published empty. A primary that has never been fetched successfully either stands in its bundled list (the default source) or blocks publishing (any other), so a first run can never replace the seed with less than it. Additions and removals are logged per source. A source that lists a known real provider, or a public suffix, is logged as a possible poisoning, and the entry is dropped. Accepting a snapshot, publishing and rolling back all happen under the same lock; downloads happen outside it.
411
+ Every size is a count of *storable* domains (not junk, not a public suffix, not a known real provider, not a privacy relay), counted the same way whatever your settings, so a payload padded with such lines cannot pass for a full list and changing `block_privacy_relays` or `excluded_domains` never trips a guard. The list is never published empty. A primary that has never been fetched successfully blocks publishing, and the current table is kept as it is. Two exceptions let the default source's bundled list stand in for it: an EMPTY table (a fresh install), and an explicit `rollback`, which must always be able to withdraw a poisoned list. So an update can never replace a fresh seed with less than it, nor a table upgraded from 0.4/0.5 (with any source configuration) with an older bundled snapshot. Additions and removals are logged per source. A source that lists a known real provider, or a public suffix, is logged as a possible poisoning, and the entry is dropped. Accepting a snapshot, publishing and rolling back all happen under the same lock; downloads happen outside it.
412
412
 
413
413
  > [!WARNING]
414
414
  > **Two concurrency limits, by design:**
@@ -303,6 +303,16 @@ module Nondisposable
303
303
  def publish!(withdrawn: nil)
304
304
  snapshots = {}
305
305
  missing_primaries = []
306
+ # The bundled list may stand in for a never-fetched primary ONLY on an
307
+ # empty table. A populated table (a host upgrading from 0.4/0.5, whose
308
+ # rows are newer than this gem's release) is kept as it is until every
309
+ # primary has an accepted snapshot of its own: replacing it with the
310
+ # bundled snapshot would silently unblock every domain added upstream
311
+ # since the release (0.6.1; reported by a CarHey codex review).
312
+ # A rollback is the deliberate exception: withdrawing a poisoned list
313
+ # must succeed even when the default source never had a snapshot of
314
+ # its own, so there the stand-in is used whatever the table holds.
315
+ table_empty = DisposableDomain.uncached { DisposableDomain.none? }
306
316
 
307
317
  configured_sources.each do |source|
308
318
  snapshot = SourceSnapshot.last_good(source.name)
@@ -310,7 +320,7 @@ module Nondisposable
310
320
  snapshots[source] = snapshot.domain_list
311
321
  elsif source.name == withdrawn
312
322
  log :warn, "#{source.name} has no good snapshot left after the rollback; building the list without it."
313
- elsif source.primary? && source.bundled_path
323
+ elsif source.primary? && source.bundled_path && (table_empty || withdrawn)
314
324
  log :warn, "#{source.name} has no good snapshot yet; using the list bundled with the gem in its place."
315
325
  snapshots[source] = Source.parse(File.read(source.bundled_path)).domains
316
326
  elsif source.primary?
@@ -322,7 +332,8 @@ module Nondisposable
322
332
 
323
333
  if missing_primaries.any?
324
334
  log :error, "Not publishing: primary source(s) #{missing_primaries.join(', ')} have never been fetched successfully. " \
325
- 'Keeping the current list (seeding it from the bundled snapshot if it is empty).'
335
+ 'Keeping the current list unchanged (seeding it from the bundled snapshot only if it is empty) ' \
336
+ 'until every primary has an accepted snapshot of its own.'
326
337
  seed!
327
338
  return false
328
339
  end
@@ -371,7 +382,9 @@ module Nondisposable
371
382
  # ---- Seeding and writing ----------------------------------------------
372
383
 
373
384
  def seed!
374
- return false unless DisposableDomain.none?
385
+ # Callers hold the publish lock, which already disables the query
386
+ # cache; asked uncached here as well so no future caller can forget.
387
+ return false unless DisposableDomain.uncached { DisposableDomain.none? }
375
388
 
376
389
  log :info, 'Domain table is empty; seeding from the blocklist bundled with the gem...'
377
390
  domains = Source.parse(bundled_domains.join("\n")).domains
@@ -465,10 +478,16 @@ module Nondisposable
465
478
  # time, and two processes publishing the same snapshots write the same
466
479
  # rows. Returns the block's value, or nil when another process held the
467
480
  # lock for longer than PUBLISH_LOCK_WAIT (it is publishing the same thing).
481
+ #
482
+ # Everything under the lock runs with the query cache OFF: an emptiness
483
+ # check (`DisposableDomain.none?`) answered from a cache filled before
484
+ # another process or thread wrote the table would let the bundled list
485
+ # replace rows it never saw.
468
486
  def with_publish_lock(&block)
487
+ uncached = -> { DisposableDomain.uncached(&block) }
469
488
  PROCESS_LOCK.synchronize do
470
489
  DisposableDomain.connection_pool.with_connection do |connection|
471
- next block.call unless connection.supports_advisory_locks?
490
+ next uncached.call unless connection.supports_advisory_locks?
472
491
 
473
492
  acquired = false
474
493
  deadline = monotonic + PUBLISH_LOCK_WAIT
@@ -482,7 +501,7 @@ module Nondisposable
482
501
  next nil unless acquired
483
502
 
484
503
  begin
485
- block.call
504
+ uncached.call
486
505
  ensure
487
506
  connection.release_advisory_lock(PUBLISH_LOCK_ID)
488
507
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nondisposable
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nondisposable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rameerez
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-17 00:00:00.000000000 Z
10
+ date: 2026-10-18 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rails