textus 0.14.1 → 0.14.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/CHANGELOG.md +25 -0
- data/lib/textus/application/refresh/worker.rb +9 -3
- data/lib/textus/cli/verb/rule_list.rb +1 -0
- data/lib/textus/domain/policy/refresh.rb +6 -5
- data/lib/textus/manifest/rules.rb +1 -0
- data/lib/textus/manifest/schema.rb +15 -2
- data/lib/textus/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a732844e2441328f7519ffd93571c3ff611fd0514c2b1f927b5c4ada499db16
|
|
4
|
+
data.tar.gz: 63a3e530439c37fbbff8c8b3864b1489cd34c88c88bd1c10f03ae980f4a66041
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5470a6aaf29f1b081144b2eca3b1e87e5b1d465977422c967366196d259041cdbb8e0993ce293d87d7903f9446649f80ea99e1e6367b9731f7b839bf18f9d67f
|
|
7
|
+
data.tar.gz: dcd3f57064d2f6753f049daf1315a937566e3b77a9ec130455a54410292f73ec55104bafe1e1b20431dc186a1c13735a7e6c6018ef8f54b77122f0eb2e267e8c
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,31 @@ 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.2 — 2026-05-26
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Per-rule `fetch_timeout_seconds:` under `rules[].refresh` overrides the
|
|
17
|
+
hardcoded 30s worker timeout that applies to intake handlers invoked
|
|
18
|
+
via the refresh pipeline (`textus refresh <key>`, `textus refresh
|
|
19
|
+
stale`, and read-path `on_stale: sync` / `timed_sync`). Default stays
|
|
20
|
+
at 30s — opt-in only. Schema validates a positive integer ≤ 3600.
|
|
21
|
+
Mirrors `sync_budget_ms` plumbing: schema → rules → policy → worker
|
|
22
|
+
(#54).
|
|
23
|
+
|
|
24
|
+
### Notes
|
|
25
|
+
|
|
26
|
+
- `fetch_timeout_seconds` is the worker-side hard cap on the intake
|
|
27
|
+
call; `sync_budget_ms` is the caller-side wait budget for `timed_sync`
|
|
28
|
+
on the read path. Two separate concerns, two separate keys.
|
|
29
|
+
- The CLI verbs `textus put --fetch` and `textus hook run` still use the
|
|
30
|
+
30s constant — only the refresh worker honors the per-rule override in
|
|
31
|
+
this release.
|
|
32
|
+
- Long timeouts pair with `Timeout.timeout`, which raises in the Ruby
|
|
33
|
+
thread but does not kill spawned subprocesses. Intake handlers that
|
|
34
|
+
shell out (e.g. `git clone`) should write to a temp dir and atomically
|
|
35
|
+
rename so mid-flight aborts leave no observable partial state.
|
|
36
|
+
|
|
12
37
|
## 0.14.1 — 2026-05-26
|
|
13
38
|
|
|
14
39
|
### Changed
|
|
@@ -26,6 +26,11 @@ module Textus
|
|
|
26
26
|
Application::Context.new(store: @ctx.store, role: @ctx.role)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
def fetch_timeout_for(key)
|
|
30
|
+
rule = @ctx.store.manifest.rules_for(key)
|
|
31
|
+
rule&.refresh&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
|
|
32
|
+
end
|
|
33
|
+
|
|
29
34
|
def fetch_with_bus(key, mentry)
|
|
30
35
|
callable = @ctx.store.registry.rpc_callable(:resolve_intake, mentry.intake_handler)
|
|
31
36
|
@bus.publish(:refresh_started, store: read_view, key: key, mode: :sync,
|
|
@@ -34,14 +39,15 @@ module Textus
|
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
def call_intake(key, mentry, callable)
|
|
37
|
-
|
|
42
|
+
timeout = fetch_timeout_for(key)
|
|
43
|
+
Timeout.timeout(timeout) do
|
|
38
44
|
callable.call(store: @ctx, config: mentry.intake_config, args: {})
|
|
39
45
|
end
|
|
40
46
|
rescue Timeout::Error
|
|
41
47
|
@bus.publish(:refresh_failed, store: read_view, key: key, error_class: "Timeout::Error",
|
|
42
|
-
error_message: "intake '#{mentry.intake_handler}' exceeded #{
|
|
48
|
+
error_message: "intake '#{mentry.intake_handler}' exceeded #{timeout}s",
|
|
43
49
|
correlation_id: @ctx.correlation_id)
|
|
44
|
-
raise UsageError.new("intake '#{mentry.intake_handler}' exceeded #{
|
|
50
|
+
raise UsageError.new("intake '#{mentry.intake_handler}' exceeded #{timeout}s timeout")
|
|
45
51
|
rescue Textus::Error => e
|
|
46
52
|
@bus.publish(:refresh_failed, store: read_view, key: key, error_class: e.class.name,
|
|
47
53
|
error_message: e.message, correlation_id: @ctx.correlation_id)
|
|
@@ -10,6 +10,7 @@ module Textus
|
|
|
10
10
|
"ttl_seconds" => b.refresh.ttl_seconds,
|
|
11
11
|
"on_stale" => b.refresh.on_stale,
|
|
12
12
|
"sync_budget_ms" => b.refresh.sync_budget_ms,
|
|
13
|
+
"fetch_timeout_seconds" => b.refresh.fetch_timeout_seconds,
|
|
13
14
|
}
|
|
14
15
|
end
|
|
15
16
|
row["handler_allowlist"] = b.handler_allowlist.handlers if b.handler_allowlist
|
|
@@ -2,9 +2,9 @@ module Textus
|
|
|
2
2
|
module Domain
|
|
3
3
|
module Policy
|
|
4
4
|
class Refresh
|
|
5
|
-
attr_reader :ttl, :on_stale, :sync_budget_ms
|
|
5
|
+
attr_reader :ttl, :on_stale, :sync_budget_ms, :fetch_timeout_seconds
|
|
6
6
|
|
|
7
|
-
def initialize(ttl:, on_stale:, sync_budget_ms:)
|
|
7
|
+
def initialize(ttl:, on_stale:, sync_budget_ms:, fetch_timeout_seconds: nil)
|
|
8
8
|
on_stale_sym = on_stale.is_a?(Symbol) ? on_stale : on_stale.to_s.to_sym
|
|
9
9
|
unless ALLOWED_ON_STALE.include?(on_stale_sym)
|
|
10
10
|
raise Textus::UsageError.new(
|
|
@@ -12,9 +12,10 @@ module Textus
|
|
|
12
12
|
)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
@ttl
|
|
16
|
-
@on_stale
|
|
17
|
-
@sync_budget_ms
|
|
15
|
+
@ttl = ttl
|
|
16
|
+
@on_stale = on_stale_sym
|
|
17
|
+
@sync_budget_ms = sync_budget_ms
|
|
18
|
+
@fetch_timeout_seconds = fetch_timeout_seconds
|
|
18
19
|
end
|
|
19
20
|
|
|
20
21
|
def ttl_seconds
|
|
@@ -11,7 +11,8 @@ module Textus
|
|
|
11
11
|
COMPUTE_KEYS = %w[kind select pluck sort_by limit transform command sources].freeze
|
|
12
12
|
INTAKE_KEYS = %w[handler config].freeze
|
|
13
13
|
RULE_KEYS = %w[match refresh intake_handler_allowlist promotion retention].freeze
|
|
14
|
-
REFRESH_KEYS = %w[ttl on_stale sync_budget_ms].freeze
|
|
14
|
+
REFRESH_KEYS = %w[ttl on_stale sync_budget_ms fetch_timeout_seconds].freeze
|
|
15
|
+
FETCH_TIMEOUT_SECONDS_CEILING = 3600
|
|
15
16
|
PROMOTION_KEYS = %w[requires].freeze
|
|
16
17
|
|
|
17
18
|
def self.validate!(raw)
|
|
@@ -30,11 +31,23 @@ module Textus
|
|
|
30
31
|
Array(raw["rules"]).each_with_index do |r, i|
|
|
31
32
|
path = "$.rules[#{i}]"
|
|
32
33
|
walk(r, RULE_KEYS, path)
|
|
33
|
-
|
|
34
|
+
if r["refresh"].is_a?(Hash)
|
|
35
|
+
walk(r["refresh"], REFRESH_KEYS, "#{path}.refresh")
|
|
36
|
+
validate_fetch_timeout!(r["refresh"]["fetch_timeout_seconds"], "#{path}.refresh.fetch_timeout_seconds")
|
|
37
|
+
end
|
|
34
38
|
walk(r["promotion"], PROMOTION_KEYS, "#{path}.promotion") if r["promotion"].is_a?(Hash)
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
def self.validate_fetch_timeout!(value, path)
|
|
43
|
+
return if value.nil?
|
|
44
|
+
return if value.is_a?(Integer) && value.positive? && value <= FETCH_TIMEOUT_SECONDS_CEILING
|
|
45
|
+
|
|
46
|
+
raise BadManifest.new(
|
|
47
|
+
"fetch_timeout_seconds at '#{path}' must be a positive integer ≤ #{FETCH_TIMEOUT_SECONDS_CEILING} (got #{value.inspect})",
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
38
51
|
def self.walk(hash, allowed, path)
|
|
39
52
|
return unless hash.is_a?(Hash)
|
|
40
53
|
|
data/lib/textus/version.rb
CHANGED