textus 0.14.3 → 0.14.4
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/CHANGELOG.md +42 -0
- data/lib/textus/application/context.rb +13 -7
- data/lib/textus/application/reads/get.rb +1 -0
- data/lib/textus/builder/pipeline.rb +1 -1
- data/lib/textus/doctor/check/refresh_locks.rb +49 -0
- data/lib/textus/doctor.rb +1 -0
- data/lib/textus/operations.rb +2 -1
- data/lib/textus/projection.rb +5 -3
- data/lib/textus/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1e2a12b234bfc677ce44ec7b49b3b04b4ac69abbc15f13496911c82d89549c8
|
|
4
|
+
data.tar.gz: 6b83336a05cb3cee509d3e0b90ce2048ed57fdeb5fbc603f22303e10c2d5c27c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f126d898385e0d0236a1818a741923f8ee9f88f1f13cab8ef37bbb810d14145904186084f72d0fbc5a4d6bfdf53b5e7e68699f56db4f9aa02d22772019a11316
|
|
7
|
+
data.tar.gz: 054ea4fea78cf26d570319d49f2a0ffbd4023e6355b56b8830650e7d59b1b57ae2de7b158ef0611b465369136e11b292ba9f968113cd425ecd1cfed1bf96633d
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,48 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
9
9
|
bump is a breaking change that requires a store migration; the gem version
|
|
10
10
|
tracks both additive improvements and breaking protocol bumps independently.
|
|
11
11
|
|
|
12
|
+
## 0.14.4 — 2026-05-26
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `textus build` no longer triggers per-leaf refresh fan-out when
|
|
17
|
+
projection reads encounter stale entries under `on_stale: timed_sync`
|
|
18
|
+
/ `on_stale: sync`. Build is a downstream materialization step over
|
|
19
|
+
current store state; freshness is an inflow concern, and they
|
|
20
|
+
shouldn't compose. Previously, building a marketplace-style output
|
|
21
|
+
whose projection selected `intake.vendor.**` against ~400 stale
|
|
22
|
+
leaves spawned ~400 concurrent detached refresh workers, each
|
|
23
|
+
re-running the full intake handler, exhausting the system. The
|
|
24
|
+
build pipeline now reads through an `Application::Context` with
|
|
25
|
+
`bypass_freshness: true`, so `Application::Reads::Get` returns the
|
|
26
|
+
on-disk envelope annotated as fresh without consulting the
|
|
27
|
+
orchestrator. Explicit freshness before build still works via
|
|
28
|
+
`textus refresh stale`. (#59)
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `Application::Context#bypass_freshness?` — new flag for read paths
|
|
33
|
+
that must not initiate refresh. Threaded through `Operations.for`
|
|
34
|
+
and propagated across `Context#with_role`. Used by `Builder::Pipeline`
|
|
35
|
+
via a `bypass_freshness:` constructor kwarg on `Projection`.
|
|
36
|
+
- `textus doctor` now reports stale per-key refresh lock files under
|
|
37
|
+
`<root>/.locks/` whose recorded PID is no longer running, as an
|
|
38
|
+
`info`-level `refresh_lock.stale` issue. The check is purely
|
|
39
|
+
informational: `Refresh::Lock` uses `flock(2)`, which the kernel
|
|
40
|
+
releases on process death, so stale `.lock` files on disk do not
|
|
41
|
+
block subsequent refresh acquires. The check exists so users can
|
|
42
|
+
clean up forensic clutter and notice unexpected accumulation. No
|
|
43
|
+
read-path changes — adding a PID probe + unlink there would
|
|
44
|
+
reintroduce the TOCTOU and PID-reuse hazards explicitly rejected
|
|
45
|
+
in 0.14.3 / PR #57. (#58)
|
|
46
|
+
|
|
47
|
+
### Tested
|
|
48
|
+
|
|
49
|
+
- Added a regression spec that forks a child, takes a per-key
|
|
50
|
+
`Refresh::Lock`, SIGKILLs the child, and asserts a fresh acquire
|
|
51
|
+
on the same key succeeds without manual cleanup. Pins the
|
|
52
|
+
flock-survives-SIGKILL contract.
|
|
53
|
+
|
|
12
54
|
## 0.14.3 — 2026-05-26
|
|
13
55
|
|
|
14
56
|
### Added
|
|
@@ -9,13 +9,14 @@ module Textus
|
|
|
9
9
|
new(store: store, role: "human")
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def initialize(store:, role:, correlation_id: nil, clock: Time, dry_run: false)
|
|
13
|
-
@store
|
|
14
|
-
@role
|
|
15
|
-
@correlation_id
|
|
16
|
-
@clock
|
|
17
|
-
@dry_run
|
|
18
|
-
@
|
|
12
|
+
def initialize(store:, role:, correlation_id: nil, clock: Time, dry_run: false, bypass_freshness: false)
|
|
13
|
+
@store = store
|
|
14
|
+
@role = role.to_s
|
|
15
|
+
@correlation_id = correlation_id || SecureRandom.uuid
|
|
16
|
+
@clock = clock
|
|
17
|
+
@dry_run = dry_run
|
|
18
|
+
@bypass_freshness = bypass_freshness
|
|
19
|
+
@now = nil
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def now
|
|
@@ -26,6 +27,10 @@ module Textus
|
|
|
26
27
|
@dry_run
|
|
27
28
|
end
|
|
28
29
|
|
|
30
|
+
def bypass_freshness?
|
|
31
|
+
@bypass_freshness
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def can_write?(zone)
|
|
30
35
|
store.manifest.permission_for(zone.to_s).allows_write?(role)
|
|
31
36
|
end
|
|
@@ -41,6 +46,7 @@ module Textus
|
|
|
41
46
|
correlation_id: @correlation_id,
|
|
42
47
|
clock: @clock,
|
|
43
48
|
dry_run: @dry_run,
|
|
49
|
+
bypass_freshness: @bypass_freshness,
|
|
44
50
|
)
|
|
45
51
|
end
|
|
46
52
|
end
|
|
@@ -11,6 +11,7 @@ module Textus
|
|
|
11
11
|
def call(key)
|
|
12
12
|
envelope = @ctx.store.reader.read_raw_envelope(key)
|
|
13
13
|
return nil if envelope.nil?
|
|
14
|
+
return annotate_fresh(envelope) if @ctx.bypass_freshness?
|
|
14
15
|
|
|
15
16
|
policy_set = @ctx.store.manifest.rules_for(key)
|
|
16
17
|
refresh_policy = policy_set.refresh
|
|
@@ -62,7 +62,7 @@ module Textus
|
|
|
62
62
|
# 1. Load sources + project + reduce
|
|
63
63
|
data =
|
|
64
64
|
if mentry.projection
|
|
65
|
-
Projection.new(store, mentry.projection).run
|
|
65
|
+
Projection.new(store, mentry.projection, bypass_freshness: true).run
|
|
66
66
|
else
|
|
67
67
|
{ "entries" => [], "count" => 0, "generated_at" => Time.now.utc.iso8601 }
|
|
68
68
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Doctor
|
|
3
|
+
class Check
|
|
4
|
+
# Lists per-key refresh lock files under <store.root>/.locks/ whose
|
|
5
|
+
# recorded PID is no longer running. These are forensic artifacts only:
|
|
6
|
+
# Refresh::Lock uses flock(2), which the kernel releases on process
|
|
7
|
+
# death, so stale files do not block subsequent acquires. The check
|
|
8
|
+
# exists to let users clean up clutter and notice unexpected accumulation
|
|
9
|
+
# (e.g. a refresh path that crashes repeatedly).
|
|
10
|
+
class RefreshLocks < Check
|
|
11
|
+
def call
|
|
12
|
+
dir = File.join(store.root, ".locks")
|
|
13
|
+
return [] unless File.directory?(dir)
|
|
14
|
+
|
|
15
|
+
Dir.glob(File.join(dir, "*.lock")).filter_map { |path| inspect_lock(path) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def inspect_lock(path)
|
|
21
|
+
pid = File.read(path).strip.to_i
|
|
22
|
+
return nil if pid.zero?
|
|
23
|
+
return nil if pid_alive?(pid)
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
"code" => "refresh_lock.stale",
|
|
27
|
+
"level" => "info",
|
|
28
|
+
"subject" => path,
|
|
29
|
+
"message" => "refresh lock file at #{path} records dead PID #{pid} " \
|
|
30
|
+
"(does not block refresh; flock is kernel-released on exit)",
|
|
31
|
+
"fix" => "safe to delete: rm #{path}",
|
|
32
|
+
}
|
|
33
|
+
rescue Errno::ENOENT
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def pid_alive?(pid)
|
|
38
|
+
Process.kill(0, pid)
|
|
39
|
+
true
|
|
40
|
+
rescue Errno::ESRCH
|
|
41
|
+
false
|
|
42
|
+
rescue Errno::EPERM
|
|
43
|
+
# Process exists but owned by another user — treat as alive.
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/textus/doctor.rb
CHANGED
data/lib/textus/operations.rb
CHANGED
|
@@ -9,12 +9,13 @@ module Textus
|
|
|
9
9
|
#
|
|
10
10
|
# Replaces the prior `Textus::Composition` module (deleted in v0.12.2).
|
|
11
11
|
class Operations
|
|
12
|
-
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false)
|
|
12
|
+
def self.for(store, role: Role::DEFAULT, correlation_id: nil, dry_run: false, bypass_freshness: false)
|
|
13
13
|
ctx = Application::Context.new(
|
|
14
14
|
store: store,
|
|
15
15
|
role: role,
|
|
16
16
|
correlation_id: correlation_id,
|
|
17
17
|
dry_run: dry_run,
|
|
18
|
+
bypass_freshness: bypass_freshness,
|
|
18
19
|
)
|
|
19
20
|
new(ctx)
|
|
20
21
|
end
|
data/lib/textus/projection.rb
CHANGED
|
@@ -6,9 +6,10 @@ module Textus
|
|
|
6
6
|
MAX_LIMIT = 1000
|
|
7
7
|
REDUCER_TIMEOUT_SECONDS = 2
|
|
8
8
|
|
|
9
|
-
def initialize(store, spec)
|
|
9
|
+
def initialize(store, spec, bypass_freshness: false)
|
|
10
10
|
@store = store
|
|
11
11
|
@spec = spec || {}
|
|
12
|
+
@bypass_freshness = bypass_freshness
|
|
12
13
|
@limit = (@spec["limit"] || MAX_LIMIT).to_i
|
|
13
14
|
raise InvalidProjection.new("limit #{@limit} exceeds max #{MAX_LIMIT}") if @limit > MAX_LIMIT
|
|
14
15
|
end
|
|
@@ -16,8 +17,9 @@ module Textus
|
|
|
16
17
|
def run
|
|
17
18
|
keys = collect_keys
|
|
18
19
|
explicit_pluck = !@spec["pluck"].nil? && @spec["pluck"] != "*"
|
|
20
|
+
ops = Operations.for(@store, bypass_freshness: @bypass_freshness)
|
|
19
21
|
rows = keys.map do |key|
|
|
20
|
-
env =
|
|
22
|
+
env = ops.reads.get.call(key)
|
|
21
23
|
row = pluck(env.meta, env.body)
|
|
22
24
|
explicit_pluck ? row : row.merge("_key" => key)
|
|
23
25
|
end
|
|
@@ -50,7 +52,7 @@ module Textus
|
|
|
50
52
|
|
|
51
53
|
def collect_keys
|
|
52
54
|
prefixes = Array(@spec["select"])
|
|
53
|
-
ops = Operations.for(@store)
|
|
55
|
+
ops = Operations.for(@store, bypass_freshness: @bypass_freshness)
|
|
54
56
|
prefixes.flat_map { |p| ops.reads.list.call(prefix: p).map { |row| row["key"] } }.uniq
|
|
55
57
|
end
|
|
56
58
|
|
data/lib/textus/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.14.
|
|
4
|
+
version: 0.14.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -188,6 +188,7 @@ files:
|
|
|
188
188
|
- lib/textus/doctor/check/intake_registration.rb
|
|
189
189
|
- lib/textus/doctor/check/manifest_files.rb
|
|
190
190
|
- lib/textus/doctor/check/protocol_version.rb
|
|
191
|
+
- lib/textus/doctor/check/refresh_locks.rb
|
|
191
192
|
- lib/textus/doctor/check/rule_ambiguity.rb
|
|
192
193
|
- lib/textus/doctor/check/schema_parse_error.rb
|
|
193
194
|
- lib/textus/doctor/check/schema_violations.rb
|