virgodb 0.4.8-x86_64-linux-gnu → 0.5.0-x86_64-linux-gnu

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: 8d126a72fa57e28991c642bd995b952c81df1f024e51291f8a880f63913f5483
4
- data.tar.gz: 48682c56e3f09417b8e241909edf65ae2519a07e0c62c27813aee63164c3bef1
3
+ metadata.gz: 9eeffeb389afab6ec06fbb371c526a2191c7422892a82d41d4c5890d70f136d7
4
+ data.tar.gz: c9410d12cb71225253f2610b01658181204dbfc71bdedf944d0e8824610fed93
5
5
  SHA512:
6
- metadata.gz: 57912dbb519d7d1c62e13ff1db65b0394f2da3a1013caa6c906aa7ec4c09892d6d029d51deaba9de5a6a5f9217c867f90b6a9a427b971e0fc0a056f6d586a59b
7
- data.tar.gz: e67f09ae131c4d5d35bbe3f505a79107a0136578b00345d109296b022911d3d4e6b2c843b894ac1916e85e70e5591fe16469743ddc1a8302c26892a356f49487
6
+ metadata.gz: e8536425d8f46324976627c7fef51a137dc7911d226ec4c9703d5db0935f1834217123b7f85d2f21b0392a1e463324b19d037aa384672b0af675311fb5c0f820
7
+ data.tar.gz: 394df85ea9b968c880ea7d81babdb8c4914afe65cc10f7e5ce137906a278bfa4fbbf814ef3d5095034e8dfdee53ec84b2f98e2515a9636d00adf7b44e38e9545
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Virgodb
4
+ # Raised by `Virgodb.with_manifest_quiesced` when a table's maintenance
5
+ # lease is still held by someone else (real background maintenance, or a
6
+ # concurrent quiesce attempt) when `lease_wait_timeout` runs out.
7
+ class LeaseUnavailable < StandardError; end
8
+
9
+ class << self
10
+ # Claims EVERY `[manifest_path, table_name]` pair in `targets`'
11
+ # maintenance lease (the SAME mutual exclusion the real background
12
+ # maintenance thread already uses against itself across processes), so
13
+ # `holder` gets exclusive access to every manifest file involved for as
14
+ # long as the block runs, then releases every claimed lease afterward
15
+ # -- whether the block succeeds or raises.
16
+ #
17
+ # Takes a `[manifest_path, table_name]` pair per target, not one shared
18
+ # `manifest_path` plus a list of table names -- deliberately, even
19
+ # though every target sharing one physical file (this project's own
20
+ # real production topology) is the common case. A caller with tables
21
+ # split across separate manifest files (a topology this gem already
22
+ # supports -- see `Registry.override_manifest_path!`-style per-table
23
+ # overrides, real in this project's own test suite) needs each table's
24
+ # lease claimed against its OWN file, not all of them against
25
+ # whichever one happens to be listed first -- collapsing to one shared
26
+ # path would silently claim/release the wrong table's lease against
27
+ # the wrong file for every target after the first.
28
+ #
29
+ # Deliberately still per-table leases underneath, not a genuine
30
+ # single manifest-scoped lease: the real maintenance thread's own
31
+ # compaction/vacuum/integrity-check only ever checks the per-table
32
+ # lease, and a brand-new separate file-level lock nothing else looks at
33
+ # would protect nothing. Collapsing the underlying lease itself to
34
+ # one-per-file would also serialize every table's maintenance behind a
35
+ # single lock, regressing the deliberate "tables' maintenance runs
36
+ # independently" property `Manifest::try_claim_maintenance_lease`'s own
37
+ # doc comment describes.
38
+ #
39
+ # Waits up to `lease_wait_timeout` seconds (polling every
40
+ # `lease_poll_interval` seconds) for a lease real maintenance currently
41
+ # holds; raises `LeaseUnavailable` if one never frees up in time. The
42
+ # final `ensure` releases every table unconditionally, not just the
43
+ # ones this call actually reached before raising -- safe because
44
+ # `release_maintenance_lease` is a no-op on a lease this holder never
45
+ # held (see that method's own doc comment), so this needs no separate
46
+ # partial-rollback bookkeeping.
47
+ #
48
+ # `renewal_interval` (seconds, optional): if given, a background thread
49
+ # re-extends every claimed lease this often for as long as the block
50
+ # runs, instead of trusting the single fixed `lease_duration_secs`
51
+ # grant from the initial claim to always be enough -- needed for a
52
+ # caller whose actual work (e.g. a slow first-ever backup upload with
53
+ # no dedup baseline yet) could genuinely outlive one fixed grant and
54
+ # free real maintenance to start compacting/vacuuming mid-work. Uses a
55
+ # background `Thread` rather than checking elapsed time inline, since
56
+ # the caller's own slow work (e.g. a blocking shellout) has no natural
57
+ # checkpoint to interleave a renewal call into; `Thread#kill` in the
58
+ # `ensure` is deliberately not a graceful join -- a lease renewal is
59
+ # just an independent, atomic single-row update per table, nothing here
60
+ # has multi-step state a mid-flight kill could corrupt. Omit (the
61
+ # default) when the block's own work is guaranteed to finish well
62
+ # within `lease_duration_secs` on its own.
63
+ #
64
+ # Extracted here (not left to live per-host-app) because this exact
65
+ # "claim every table sharing a manifest, do work, release every table"
66
+ # dance has no host-app-specific content in it at all -- any embedder
67
+ # needing exclusive access to a shared manifest for an external
68
+ # operation (a backup tool, a manual admin task, anything else) needs
69
+ # the identical coordination, and every independent reimplementation of
70
+ # it is a real chance for the per-table lease and whatever whole-file
71
+ # operation it's meant to protect to drift out of sync (see this
72
+ # project's own README, the 2026-09 incremental_vacuum incident, for
73
+ # exactly that failure shape once).
74
+ def with_manifest_quiesced(targets:, holder:, lease_duration_secs:, lease_wait_timeout:, lease_poll_interval:, renewal_interval: nil)
75
+ holder = holder.to_s
76
+ targets.each { |manifest_path, table_name| claim_lease!(manifest_path, table_name, holder: holder, lease_duration_secs: lease_duration_secs, lease_wait_timeout: lease_wait_timeout, lease_poll_interval: lease_poll_interval) }
77
+
78
+ if renewal_interval
79
+ with_lease_renewal(targets, holder: holder, lease_duration_secs: lease_duration_secs, renewal_interval: renewal_interval) { yield }
80
+ else
81
+ yield
82
+ end
83
+ ensure
84
+ targets.each { |manifest_path, table_name| release_maintenance_lease(manifest_path, table_name, holder) }
85
+ end
86
+
87
+ private
88
+
89
+ def claim_lease!(manifest_path, table_name, holder:, lease_duration_secs:, lease_wait_timeout:, lease_poll_interval:)
90
+ deadline = Time.now + lease_wait_timeout
91
+ until claim_maintenance_lease(manifest_path, table_name, holder, lease_duration_secs)
92
+ if Time.now > deadline
93
+ raise LeaseUnavailable, "could not claim #{table_name}'s maintenance lease within #{lease_wait_timeout}s (still held by real maintenance)"
94
+ end
95
+
96
+ sleep lease_poll_interval
97
+ end
98
+ end
99
+
100
+ def with_lease_renewal(targets, holder:, lease_duration_secs:, renewal_interval:)
101
+ renewal_thread = Thread.new do
102
+ loop do
103
+ sleep renewal_interval
104
+ targets.each { |manifest_path, table_name| renew_maintenance_lease(manifest_path, table_name, holder, lease_duration_secs) }
105
+ end
106
+ end
107
+ yield
108
+ ensure
109
+ renewal_thread&.kill
110
+ end
111
+ end
112
+ end
data/lib/virgodb/table.rb CHANGED
@@ -120,6 +120,18 @@ module Virgodb
120
120
  Virgodb.integrity_check(@manifest_path)
121
121
  end
122
122
 
123
+ # Writes a complete, consistent snapshot of the manifest to dest_path
124
+ # via VACUUM INTO, through virgodb's own connection -- same "don't use
125
+ # a second connection type against this shared file" reasoning as
126
+ # integrity_check above, just for backup tooling instead of health
127
+ # checks. Manifest-scoped, not table-scoped. See
128
+ # Manifest::snapshot_into's own doc comment (crates/manifest/src/lib.rs)
129
+ # for what VACUUM INTO itself guarantees and its one real caller
130
+ # constraint (refuses to overwrite an existing dest_path).
131
+ def snapshot(dest_path)
132
+ Virgodb.snapshot(@manifest_path, dest_path)
133
+ end
134
+
123
135
  # Claims this table's maintenance lease as `holder` for the next
124
136
  # `lease_duration_secs` seconds, locking out the real maintenance
125
137
  # thread (its own claim attempts fail against an unexpired lease the
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Virgodb
4
- VERSION = "0.4.8"
4
+ VERSION = "0.5.0"
5
5
  end
Binary file
data/lib/virgodb.rb CHANGED
@@ -5,3 +5,4 @@ require "virgodb/virgodb_ruby"
5
5
  require "virgodb/time_helpers"
6
6
  require "virgodb/rows"
7
7
  require "virgodb/table"
8
+ require "virgodb/manifest_quiesce"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: virgodb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.8
4
+ version: 0.5.0
5
5
  platform: x86_64-linux-gnu
6
6
  authors:
7
7
  - virgodb
@@ -21,6 +21,7 @@ extra_rdoc_files: []
21
21
  files:
22
22
  - LICENSE
23
23
  - lib/virgodb.rb
24
+ - lib/virgodb/manifest_quiesce.rb
24
25
  - lib/virgodb/rows.rb
25
26
  - lib/virgodb/table.rb
26
27
  - lib/virgodb/time_helpers.rb