textus 0.52.0 → 0.54.0
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 +59 -0
- data/README.md +62 -54
- data/SPEC.md +64 -192
- data/docs/architecture/README.md +157 -156
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +44 -0
- data/lib/textus/action/audit.rb +131 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +28 -22
- data/lib/textus/action/boot.rb +17 -0
- data/lib/textus/action/data_mv.rb +69 -0
- data/lib/textus/action/deps.rb +34 -0
- data/lib/textus/action/doctor.rb +24 -0
- data/lib/textus/action/drain.rb +39 -0
- data/lib/textus/action/enqueue.rb +53 -0
- data/lib/textus/action/get.rb +78 -0
- data/lib/textus/action/ingest.rb +135 -0
- data/lib/textus/action/jobs.rb +36 -0
- data/lib/textus/action/key_delete.rb +36 -0
- data/lib/textus/action/key_delete_prefix.rb +44 -0
- data/lib/textus/action/key_mv.rb +129 -0
- data/lib/textus/action/key_mv_prefix.rb +57 -0
- data/lib/textus/action/list.rb +42 -0
- data/lib/textus/action/propose.rb +52 -0
- data/lib/textus/action/published.rb +24 -0
- data/lib/textus/action/pulse.rb +56 -0
- data/lib/textus/action/put.rb +52 -0
- data/lib/textus/action/rdeps.rb +39 -0
- data/lib/textus/action/reject.rb +37 -0
- data/lib/textus/action/rule_explain.rb +91 -0
- data/lib/textus/action/rule_lint.rb +68 -0
- data/lib/textus/action/rule_list.rb +42 -0
- data/lib/textus/action/schema_envelope.rb +29 -0
- data/lib/textus/action/uid.rb +33 -0
- data/lib/textus/action/where.rb +36 -0
- data/lib/textus/action/write_verb.rb +44 -0
- data/lib/textus/boot.rb +64 -132
- data/lib/textus/command.rb +41 -0
- data/lib/textus/container.rb +1 -1
- data/lib/textus/{domain → core}/duration.rb +1 -1
- data/lib/textus/{domain → core}/freshness/evaluator.rb +25 -41
- data/lib/textus/{domain → core}/freshness/verdict.rb +2 -2
- data/lib/textus/{domain → core}/freshness.rb +2 -2
- data/lib/textus/{domain → core}/retention/sweep.rb +7 -7
- data/lib/textus/{domain → core}/retention.rb +2 -2
- data/lib/textus/{domain → core}/sentinel.rb +1 -1
- data/lib/textus/doctor/check/generator_drift.rb +1 -1
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/notebook_sources.rb +50 -0
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- data/lib/textus/doctor/check/raw_asset_paths.rb +50 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +2 -2
- data/lib/textus/doctor/check/schema_violations.rb +8 -2
- data/lib/textus/doctor/check.rb +11 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +4 -44
- data/lib/textus/envelope/reader.rb +46 -0
- data/lib/textus/envelope/writer.rb +209 -0
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +25 -13
- data/lib/textus/{entry → format}/base.rb +1 -6
- data/lib/textus/{entry → format}/json.rb +1 -4
- data/lib/textus/{entry → format}/markdown.rb +2 -6
- data/lib/textus/{entry → format}/text.rb +2 -6
- data/lib/textus/{entry → format}/yaml.rb +1 -4
- data/lib/textus/{entry.rb → format.rb} +9 -10
- data/lib/textus/gate/auth.rb +227 -0
- data/lib/textus/gate.rb +116 -0
- data/lib/textus/init.rb +59 -105
- data/lib/textus/jobs/base.rb +23 -0
- data/lib/textus/jobs/materialize.rb +20 -0
- data/lib/textus/jobs/plan.rb +9 -0
- data/lib/textus/jobs/planner.rb +101 -0
- data/lib/textus/jobs/retention.rb +48 -0
- data/lib/textus/jobs/sweep.rb +27 -0
- data/lib/textus/jobs/worker.rb +67 -0
- data/lib/textus/jobs.rb +15 -0
- data/lib/textus/key/path.rb +10 -3
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +14 -25
- data/lib/textus/manifest/entry/parser.rb +31 -14
- data/lib/textus/manifest/entry/produced.rb +13 -33
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +4 -3
- data/lib/textus/manifest/entry/publish.rb +0 -12
- data/lib/textus/manifest/entry/validators/format_matrix.rb +1 -1
- data/lib/textus/manifest/entry/validators.rb +0 -1
- data/lib/textus/{domain → manifest}/policy/matcher.rb +2 -2
- data/lib/textus/{domain → manifest}/policy/publish_target.rb +2 -2
- data/lib/textus/manifest/policy/react.rb +30 -0
- data/lib/textus/{domain → manifest}/policy/retention.rb +3 -3
- data/lib/textus/manifest/policy/source.rb +30 -0
- data/lib/textus/manifest/policy.rb +38 -53
- data/lib/textus/manifest/resolver.rb +5 -4
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +20 -28
- data/lib/textus/manifest/schema/validator.rb +27 -33
- data/lib/textus/manifest/schema/vocabulary.rb +2 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/{domain/jobs → ports/job_store}/job.rb +19 -12
- data/lib/textus/ports/{queue.rb → job_store.rb} +7 -14
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/produce/engine.rb +25 -70
- data/lib/textus/produce/render.rb +2 -7
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/store.rb +12 -19
- data/lib/textus/surfaces/cli/group/data.rb +11 -0
- data/lib/textus/surfaces/cli/group/key.rb +11 -0
- data/lib/textus/surfaces/cli/group/mcp.rb +11 -0
- data/lib/textus/surfaces/cli/group/rule.rb +11 -0
- data/lib/textus/surfaces/cli/group/schema.rb +11 -0
- data/lib/textus/surfaces/cli/group.rb +50 -0
- data/lib/textus/surfaces/cli/runner.rb +236 -0
- data/lib/textus/surfaces/cli/verb/doctor.rb +21 -0
- data/lib/textus/surfaces/cli/verb/get.rb +21 -0
- data/lib/textus/surfaces/cli/verb/init.rb +20 -0
- data/lib/textus/surfaces/cli/verb/mcp_serve.rb +24 -0
- data/lib/textus/surfaces/cli/verb/put.rb +30 -0
- data/lib/textus/surfaces/cli/verb/schema_diff.rb +17 -0
- data/lib/textus/surfaces/cli/verb/schema_init.rb +21 -0
- data/lib/textus/surfaces/cli/verb/schema_migrate.rb +21 -0
- data/lib/textus/surfaces/cli/verb/watch.rb +19 -0
- data/lib/textus/surfaces/cli/verb.rb +111 -0
- data/lib/textus/surfaces/cli.rb +148 -0
- data/lib/textus/surfaces/mcp/catalog.rb +99 -0
- data/lib/textus/surfaces/mcp/errors.rb +25 -0
- data/lib/textus/surfaces/mcp/routing.rb +51 -0
- data/lib/textus/surfaces/mcp/server.rb +184 -0
- data/lib/textus/surfaces/mcp/session.rb +9 -0
- data/lib/textus/surfaces/mcp/tool_schemas.rb +17 -0
- data/lib/textus/surfaces/mcp.rb +8 -0
- data/lib/textus/surfaces/role_scope.rb +34 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/collector.rb +27 -0
- data/lib/textus/workflow/context.rb +5 -0
- data/lib/textus/workflow/dsl.rb +44 -0
- data/lib/textus/workflow/loader.rb +17 -0
- data/lib/textus/workflow/pattern.rb +18 -0
- data/lib/textus/workflow/registry.rb +21 -0
- data/lib/textus/workflow/runner.rb +84 -0
- data/lib/textus/workflow.rb +28 -0
- data/lib/textus.rb +75 -22
- metadata +118 -132
- data/lib/textus/cli/group/hook.rb +0 -9
- data/lib/textus/cli/group/key.rb +0 -9
- data/lib/textus/cli/group/mcp.rb +0 -9
- data/lib/textus/cli/group/rule.rb +0 -9
- data/lib/textus/cli/group/schema.rb +0 -9
- data/lib/textus/cli/group/zone.rb +0 -9
- data/lib/textus/cli/group.rb +0 -48
- data/lib/textus/cli/runner.rb +0 -193
- data/lib/textus/cli/verb/doctor.rb +0 -17
- data/lib/textus/cli/verb/get.rb +0 -18
- data/lib/textus/cli/verb/hook_run.rb +0 -48
- data/lib/textus/cli/verb/hooks.rb +0 -50
- data/lib/textus/cli/verb/init.rb +0 -18
- data/lib/textus/cli/verb/mcp_serve.rb +0 -22
- data/lib/textus/cli/verb/put.rb +0 -30
- data/lib/textus/cli/verb/schema_diff.rb +0 -15
- data/lib/textus/cli/verb/schema_init.rb +0 -19
- data/lib/textus/cli/verb/schema_migrate.rb +0 -19
- data/lib/textus/cli/verb/serve.rb +0 -19
- data/lib/textus/cli/verb.rb +0 -116
- data/lib/textus/cli.rb +0 -138
- data/lib/textus/dispatcher.rb +0 -54
- data/lib/textus/doctor/check/handler_allowlist.rb +0 -34
- data/lib/textus/doctor/check/hooks.rb +0 -32
- data/lib/textus/doctor/check/intake_registration.rb +0 -46
- data/lib/textus/domain/action.rb +0 -9
- data/lib/textus/domain/jobs/registry.rb +0 -37
- data/lib/textus/domain/permission.rb +0 -7
- data/lib/textus/domain/policy/base_guards.rb +0 -25
- data/lib/textus/domain/policy/evaluation.rb +0 -15
- data/lib/textus/domain/policy/guard.rb +0 -35
- data/lib/textus/domain/policy/guard_factory.rb +0 -40
- data/lib/textus/domain/policy/handler_allowlist.rb +0 -17
- data/lib/textus/domain/policy/predicates/author_held.rb +0 -33
- data/lib/textus/domain/policy/predicates/etag_match.rb +0 -32
- data/lib/textus/domain/policy/predicates/fresh_within.rb +0 -59
- data/lib/textus/domain/policy/predicates/registry.rb +0 -39
- data/lib/textus/domain/policy/predicates/schema_valid.rb +0 -61
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +0 -33
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +0 -39
- data/lib/textus/domain/policy/source.rb +0 -73
- data/lib/textus/envelope/io/reader.rb +0 -48
- data/lib/textus/envelope/io/writer.rb +0 -182
- data/lib/textus/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/catalog.rb +0 -37
- data/lib/textus/hooks/context.rb +0 -72
- data/lib/textus/hooks/error_log.rb +0 -32
- data/lib/textus/hooks/event_bus.rb +0 -114
- data/lib/textus/hooks/fire_report.rb +0 -23
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- data/lib/textus/hooks/signature.rb +0 -31
- data/lib/textus/init/templates/machine_intake.rb +0 -45
- data/lib/textus/init/templates/orientation_reducer.rb +0 -17
- data/lib/textus/jobs/handlers.rb +0 -62
- data/lib/textus/jobs/scheduler.rb +0 -36
- data/lib/textus/jobs/seeder.rb +0 -57
- data/lib/textus/maintenance/drain.rb +0 -42
- data/lib/textus/maintenance/key_delete_prefix.rb +0 -48
- data/lib/textus/maintenance/key_mv_prefix.rb +0 -68
- data/lib/textus/maintenance/retention/apply.rb +0 -52
- data/lib/textus/maintenance/rule_lint.rb +0 -66
- data/lib/textus/maintenance/serve.rb +0 -30
- data/lib/textus/maintenance/worker.rb +0 -74
- data/lib/textus/maintenance/zone_mv.rb +0 -64
- data/lib/textus/maintenance.rb +0 -15
- data/lib/textus/manifest/entry/validators/events.rb +0 -22
- data/lib/textus/mcp/catalog.rb +0 -70
- data/lib/textus/mcp/errors.rb +0 -32
- data/lib/textus/mcp/server.rb +0 -138
- data/lib/textus/mcp/session.rb +0 -7
- data/lib/textus/mcp/tool_schemas.rb +0 -15
- data/lib/textus/mcp.rb +0 -6
- data/lib/textus/mustache.rb +0 -117
- data/lib/textus/ports/audit_subscriber.rb +0 -42
- data/lib/textus/ports/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/acquire/handler.rb +0 -29
- data/lib/textus/produce/acquire/intake.rb +0 -130
- data/lib/textus/produce/acquire/projection.rb +0 -127
- data/lib/textus/produce/acquire/serializer/json.rb +0 -31
- data/lib/textus/produce/acquire/serializer/text.rb +0 -16
- data/lib/textus/produce/acquire/serializer/yaml.rb +0 -31
- data/lib/textus/produce/acquire/serializer.rb +0 -17
- data/lib/textus/produce/events.rb +0 -36
- data/lib/textus/projection.rb +0 -103
- data/lib/textus/read/audit.rb +0 -130
- data/lib/textus/read/boot.rb +0 -26
- data/lib/textus/read/capabilities.rb +0 -70
- data/lib/textus/read/deps.rb +0 -38
- data/lib/textus/read/doctor.rb +0 -27
- data/lib/textus/read/freshness.rb +0 -152
- data/lib/textus/read/get.rb +0 -73
- data/lib/textus/read/jobs.rb +0 -31
- data/lib/textus/read/list.rb +0 -24
- data/lib/textus/read/published.rb +0 -22
- data/lib/textus/read/pulse.rb +0 -98
- data/lib/textus/read/rdeps.rb +0 -39
- data/lib/textus/read/rule_explain.rb +0 -96
- data/lib/textus/read/rule_list.rb +0 -54
- data/lib/textus/read/schema_envelope.rb +0 -25
- data/lib/textus/read/uid.rb +0 -29
- data/lib/textus/read/validate_all.rb +0 -36
- data/lib/textus/read/where.rb +0 -24
- data/lib/textus/role_scope.rb +0 -78
- data/lib/textus/write/accept.rb +0 -58
- data/lib/textus/write/enqueue.rb +0 -50
- data/lib/textus/write/key_delete.rb +0 -65
- data/lib/textus/write/key_mv.rb +0 -141
- data/lib/textus/write/propose.rb +0 -54
- data/lib/textus/write/put.rb +0 -74
- data/lib/textus/write/reject.rb +0 -68
data/lib/textus/hooks/context.rb
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
# A narrow handle passed to user hooks in place of the raw Store.
|
|
6
|
-
# All writes route back through the RoleScope so authorization, audit
|
|
7
|
-
# logging, and schema validation always fire.
|
|
8
|
-
class Context
|
|
9
|
-
attr_reader :role, :correlation_id
|
|
10
|
-
|
|
11
|
-
def self.for(container:, call:)
|
|
12
|
-
scope = Textus::RoleScope.new(
|
|
13
|
-
container: container,
|
|
14
|
-
role: call.role,
|
|
15
|
-
correlation_id: call.correlation_id,
|
|
16
|
-
dry_run: call.dry_run,
|
|
17
|
-
)
|
|
18
|
-
new(scope: scope)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def initialize(scope:)
|
|
22
|
-
@scope = scope
|
|
23
|
-
@role = scope.role
|
|
24
|
-
@correlation_id = scope.correlation_id
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def backend
|
|
28
|
-
@scope
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# read — a pure-observation surface: nothing here ingests. Since ADR 0089
|
|
32
|
-
# `get` itself is a pure read (the read-through that once forced this
|
|
33
|
-
# surface to opt out is gone, so the old re-entrancy/deadlock guard is no
|
|
34
|
-
# longer needed); `list`/`deps`/`freshness` are reads too. A hook observes
|
|
35
|
-
# current state and never triggers an I/O cascade.
|
|
36
|
-
def get(key) = pure_reader.call(key)
|
|
37
|
-
def list(**) = @scope.list(**)
|
|
38
|
-
def deps(key) = @scope.deps(key)
|
|
39
|
-
def freshness(key) = @scope.freshness(key)
|
|
40
|
-
|
|
41
|
-
# write (authorized + audited)
|
|
42
|
-
def put(key, **) = @scope.put(key, **)
|
|
43
|
-
def delete(key, **) = @scope.key_delete(key, **)
|
|
44
|
-
|
|
45
|
-
def audit(verb, key:, **)
|
|
46
|
-
@scope.container.audit_log.append(role: @role, verb: verb, key: key, **)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# fan-out
|
|
50
|
-
def publish_followup(event, **)
|
|
51
|
-
@scope.container.events.publish(event, ctx: self, **)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def inspect
|
|
55
|
-
"#<Textus::Hooks::Context role=#{@role} correlation_id=#{@correlation_id}>"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
private
|
|
59
|
-
|
|
60
|
-
def pure_reader
|
|
61
|
-
@pure_reader ||= Textus::Read::Get.new(
|
|
62
|
-
container: @scope.container,
|
|
63
|
-
call: Textus::Call.build(
|
|
64
|
-
role: @scope.role,
|
|
65
|
-
correlation_id: @scope.correlation_id,
|
|
66
|
-
dry_run: @scope.dry_run?,
|
|
67
|
-
),
|
|
68
|
-
)
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Hooks
|
|
3
|
-
# Bounded in-memory ring buffer of recent hook failures (errored and
|
|
4
|
-
# timed_out). Each row carries the audit `seq` observed at the time of
|
|
5
|
-
# failure so pulse can filter "errors since cursor".
|
|
6
|
-
class ErrorLog
|
|
7
|
-
DEFAULT_CAPACITY = 256
|
|
8
|
-
|
|
9
|
-
def initialize(capacity: DEFAULT_CAPACITY)
|
|
10
|
-
@capacity = capacity
|
|
11
|
-
@rows = []
|
|
12
|
-
@mutex = Mutex.new
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def record(seq:, event:, hook:, key:, error_class:, error_message:)
|
|
16
|
-
row = {
|
|
17
|
-
seq: seq, event: event, hook: hook, key: key,
|
|
18
|
-
error_class: error_class, error_message: error_message,
|
|
19
|
-
at: Time.now.utc.iso8601
|
|
20
|
-
}
|
|
21
|
-
@mutex.synchronize do
|
|
22
|
-
@rows << row
|
|
23
|
-
@rows.shift while @rows.size > @capacity
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def since(seq)
|
|
28
|
-
@mutex.synchronize { @rows.select { |r| r[:seq] > seq }.dup }
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
class EventBus
|
|
6
|
-
HOOK_TIMEOUT_SECONDS = 2
|
|
7
|
-
|
|
8
|
-
class HookTimeout < StandardError; end
|
|
9
|
-
|
|
10
|
-
def initialize(error_log: ErrorLog.new)
|
|
11
|
-
@pubsub = Hash.new { |h, k| h[k] = [] }
|
|
12
|
-
@error_handlers = []
|
|
13
|
-
@error_log = error_log
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
attr_reader :error_log
|
|
17
|
-
|
|
18
|
-
def on(event, name, keys: nil, &) = register(event, name, keys: keys, &)
|
|
19
|
-
|
|
20
|
-
def register(event, name, keys: nil, &blk)
|
|
21
|
-
event_sym = event.to_sym
|
|
22
|
-
raise UsageError.new("#{event_sym} is an RPC event; register on RpcRegistry") if Catalog::RPC.key?(event_sym)
|
|
23
|
-
|
|
24
|
-
required = Catalog::PUBSUB[event_sym] or raise UsageError.new("unknown event: #{event}")
|
|
25
|
-
sig = Signature.new(blk)
|
|
26
|
-
missing = sig.missing(required)
|
|
27
|
-
if missing.any?
|
|
28
|
-
raise UsageError.new("#{event_sym} hooks must accept kwargs: #{required.join(", ")} (missing: #{missing.join(", ")})")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
name = name.to_sym
|
|
32
|
-
raise UsageError.new("#{event_sym} hook '#{name}' already registered") if @pubsub[event_sym].any? { |h| h[:name] == name }
|
|
33
|
-
|
|
34
|
-
@pubsub[event_sym] << { name: name, callable: blk, keys: keys }
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def on_error(&block) = @error_handlers << block
|
|
38
|
-
|
|
39
|
-
def listeners(event, key:) = @pubsub[event.to_sym].select { |h| match?(h[:keys], key) }
|
|
40
|
-
|
|
41
|
-
def pubsub_handlers(event) = @pubsub[event.to_sym]
|
|
42
|
-
|
|
43
|
-
def publish(event, strict: false, **kwargs)
|
|
44
|
-
key = kwargs[:key] || "-"
|
|
45
|
-
fired = []
|
|
46
|
-
errored = []
|
|
47
|
-
timed_out = []
|
|
48
|
-
raised = nil
|
|
49
|
-
|
|
50
|
-
@pubsub[event.to_sym].each do |sub|
|
|
51
|
-
next unless match?(sub[:keys], key)
|
|
52
|
-
|
|
53
|
-
outcome, err = invoke(event, sub, key, kwargs)
|
|
54
|
-
case outcome
|
|
55
|
-
when :ok then fired << sub[:name]
|
|
56
|
-
when :errored then errored << sub[:name]
|
|
57
|
-
when :timed_out then timed_out << sub[:name]
|
|
58
|
-
end
|
|
59
|
-
raised ||= err if strict && err
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
raise raised if strict && raised
|
|
63
|
-
|
|
64
|
-
FireReport.new(fired: fired, errored: errored, timed_out: timed_out)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
def invoke(event, sub, key, kwargs)
|
|
70
|
-
accepted = Signature.new(sub[:callable]).filter(kwargs)
|
|
71
|
-
error = nil
|
|
72
|
-
# Thread#kill is unsafe in general but bounded here: post-commit, isolated, only a runaway user hook is affected.
|
|
73
|
-
thread = Thread.new do
|
|
74
|
-
sub[:callable].call(**accepted)
|
|
75
|
-
rescue StandardError => e
|
|
76
|
-
error = e
|
|
77
|
-
end
|
|
78
|
-
if thread.join(HOOK_TIMEOUT_SECONDS).nil?
|
|
79
|
-
thread.kill
|
|
80
|
-
err = HookTimeout.new("hook #{sub[:name]} exceeded #{HOOK_TIMEOUT_SECONDS}s on event #{event}")
|
|
81
|
-
notify_error(event, sub, key, kwargs, err)
|
|
82
|
-
return [:timed_out, err]
|
|
83
|
-
end
|
|
84
|
-
if error
|
|
85
|
-
notify_error(event, sub, key, kwargs, error)
|
|
86
|
-
return [:errored, error]
|
|
87
|
-
end
|
|
88
|
-
[:ok, nil]
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def notify_error(event, sub, key, kwargs, error)
|
|
92
|
-
@error_log.record(
|
|
93
|
-
seq: kwargs[:_audit_seq] || -1,
|
|
94
|
-
event: event,
|
|
95
|
-
hook: sub[:name],
|
|
96
|
-
key: key,
|
|
97
|
-
error_class: error.class.name,
|
|
98
|
-
error_message: error.message,
|
|
99
|
-
)
|
|
100
|
-
@error_handlers.each do |handler|
|
|
101
|
-
handler.call(event: event, hook: sub[:name], key: key, kwargs: kwargs, error: error)
|
|
102
|
-
rescue StandardError => e
|
|
103
|
-
warn "[textus] error handler failed: #{e.class}: #{e.message}"
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
def match?(globs, key)
|
|
108
|
-
return true if globs.nil?
|
|
109
|
-
|
|
110
|
-
Array(globs).any? { |g| File.fnmatch?(g, key.to_s, File::FNM_PATHNAME) }
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
# Outcome of a single Dispatcher#publish call.
|
|
6
|
-
#
|
|
7
|
-
# fired — hook names that ran to completion
|
|
8
|
-
# errored — hook names that raised
|
|
9
|
-
# timed_out — hook names whose worker thread exceeded the deadline
|
|
10
|
-
#
|
|
11
|
-
# Callers that care about hook health (tests, strict embedders) can
|
|
12
|
-
# check #ok? or inspect #failures. The dispatcher itself never raises
|
|
13
|
-
# on a hook failure unless strict: true was passed to #publish.
|
|
14
|
-
FireReport = Data.define(:fired, :errored, :timed_out) do
|
|
15
|
-
def initialize(fired:, errored:, timed_out:)
|
|
16
|
-
super(fired: fired.dup.freeze, errored: errored.dup.freeze, timed_out: timed_out.dup.freeze)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def ok? = errored.empty? && timed_out.empty?
|
|
20
|
-
def failures = errored + timed_out
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
data/lib/textus/hooks/loader.rb
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Hooks
|
|
3
|
-
class Loader
|
|
4
|
-
# A small DSL object passed to user hook blocks. Routes `.on(...)` to the
|
|
5
|
-
# EventBus and `.rpc(...)` / `.register(...)` to the RpcRegistry.
|
|
6
|
-
class Dsl
|
|
7
|
-
def initialize(events:, rpc:)
|
|
8
|
-
@events = events
|
|
9
|
-
@rpc = rpc
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
# Pubsub registration — delegates to EventBus.
|
|
13
|
-
# Also handles RPC event names by delegating to RpcRegistry.
|
|
14
|
-
def on(event, name, keys: nil, &)
|
|
15
|
-
if Hooks::Catalog::RPC.key?(event.to_sym)
|
|
16
|
-
@rpc.register(event, name, &)
|
|
17
|
-
else
|
|
18
|
-
@events.register(event, name, keys: keys, &)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Explicit RPC registration.
|
|
23
|
-
def register(event, name, &)
|
|
24
|
-
@rpc.register(event, name, &)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def initialize(events:, rpc:)
|
|
29
|
-
@events = events
|
|
30
|
-
@rpc = rpc
|
|
31
|
-
@dsl = Dsl.new(events: @events, rpc: @rpc)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def load_dir(dir)
|
|
35
|
-
return unless File.directory?(dir)
|
|
36
|
-
|
|
37
|
-
# Discard any leftover blocks from a prior partial load.
|
|
38
|
-
Textus.drain_hook_blocks
|
|
39
|
-
|
|
40
|
-
Dir.glob(File.join(dir, "**/*.rb")).sort.each do |f| # rubocop:disable Lint/RedundantDirGlobSort
|
|
41
|
-
load(f)
|
|
42
|
-
rescue StandardError, ScriptError => e
|
|
43
|
-
raise UsageError.new("failed loading hook #{File.basename(f)}: #{e.class}: #{e.message}")
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
Textus.drain_hook_blocks.each do |blk|
|
|
47
|
-
blk.call(@dsl)
|
|
48
|
-
rescue StandardError, ScriptError => e
|
|
49
|
-
raise UsageError.new("failed registering hook: #{e.class}: #{e.message}")
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
class RpcRegistry
|
|
6
|
-
def initialize
|
|
7
|
-
@table = Hash.new { |h, k| h[k] = {} }
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def register(event, name, &blk)
|
|
11
|
-
event_sym = event.to_sym
|
|
12
|
-
raise UsageError.new("#{event_sym} is a pubsub event; register on EventBus") if Catalog::PUBSUB.key?(event_sym)
|
|
13
|
-
|
|
14
|
-
required = Catalog::RPC[event_sym] or raise UsageError.new("unknown RPC event: #{event}")
|
|
15
|
-
sig = Signature.new(blk)
|
|
16
|
-
missing = sig.missing(required)
|
|
17
|
-
raise UsageError.new("#{event_sym} RPC must accept kwargs: #{required.join(", ")} (missing: #{missing.join(", ")})") if missing.any?
|
|
18
|
-
|
|
19
|
-
name = name.to_sym
|
|
20
|
-
raise UsageError.new("#{event_sym} '#{name}' already registered") if @table[event_sym].key?(name)
|
|
21
|
-
|
|
22
|
-
@table[event_sym][name] = blk
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def names(event) = @table[event.to_sym].keys
|
|
26
|
-
|
|
27
|
-
def callable(event, name)
|
|
28
|
-
@table[event.to_sym][name.to_sym] or raise UsageError.new("unknown #{event}: #{name}")
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
# Invoke a registered callable, injecting `caps:` only if the callable
|
|
32
|
-
# declares it (or accepts keyrest). Mis-named kwargs (e.g. the legacy
|
|
33
|
-
# `store:`) are rejected at registration time, not here.
|
|
34
|
-
def invoke(event, name, caps:, **other)
|
|
35
|
-
blk = callable(event, name)
|
|
36
|
-
sig = Signature.new(blk)
|
|
37
|
-
kwargs = other.dup
|
|
38
|
-
kwargs[:caps] = caps if sig.accepts_keyrest? || sig.declared_keys.include?(:caps)
|
|
39
|
-
blk.call(**kwargs)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Hooks
|
|
5
|
-
class Signature
|
|
6
|
-
def initialize(callable)
|
|
7
|
-
@params = callable.parameters
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def accepts_keyrest?
|
|
11
|
-
@params.any? { |type, _| type == :keyrest }
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def declared_keys
|
|
15
|
-
@params.each_with_object([]) { |(t, n), acc| acc << n if %i[keyreq key].include?(t) }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def missing(required)
|
|
19
|
-
return [] if accepts_keyrest?
|
|
20
|
-
|
|
21
|
-
required - declared_keys
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def filter(kwargs)
|
|
25
|
-
return kwargs if accepts_keyrest?
|
|
26
|
-
|
|
27
|
-
kwargs.slice(*declared_keys)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# .textus/hooks/machine_intake.rb
|
|
2
|
-
# Scaffolded by `textus init` — CUSTOMIZE FREELY, or delete the feeds.machines
|
|
3
|
-
# entry from manifest.yaml if you don't want it.
|
|
4
|
-
# Feeds a per-host SNAPSHOT into feeds.machines.<host> on `textus drain`
|
|
5
|
-
# (never on the per-turn boot/pulse path). It is NESTED so it grows to a fleet: the
|
|
6
|
-
# `local` leaf scans THIS host; add ssh hosts with the cookbook recipe
|
|
7
|
-
# (docs/cookbook/environment-scan.md). tracked:false → gitignored. Keep this an
|
|
8
|
-
# ALLOWLIST of versions and counts — NEVER secrets, raw `env`, or package lists.
|
|
9
|
-
Textus.hook do |reg|
|
|
10
|
-
reg.on(:resolve_handler, :machines) do |config:, args:, **|
|
|
11
|
-
machine = args[:leaf_segments].first or
|
|
12
|
-
raise "machines intake needs a host leaf, e.g. the 'local' in feeds.machines.local"
|
|
13
|
-
spec = (config["machines"] || {}).fetch(machine) { raise "unknown machine: #{machine}" }
|
|
14
|
-
unless (spec["via"] || "local").to_s == "local"
|
|
15
|
-
raise "machine #{machine}: only `via: local` is scaffolded — see " \
|
|
16
|
-
"docs/cookbook/environment-scan.md for the SSH (remote) fan-out"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
sh = ->(cmd) { `#{cmd}`.strip } # local shell-out, no network
|
|
20
|
-
ver = ->(cmd) { o = `#{cmd} 2>/dev/null`.strip; o.empty? ? nil : o } # nil if tool absent
|
|
21
|
-
count = ->(cmd) { n = `#{cmd} 2>/dev/null`.strip.lines.size; n.zero? ? nil : n }
|
|
22
|
-
{ content: {
|
|
23
|
-
# git_* describe THIS repo on the control host — only meaningful for `local`.
|
|
24
|
-
"git_head" => sh.call("git rev-parse --short HEAD 2>/dev/null"),
|
|
25
|
-
"git_branch" => sh.call("git rev-parse --abbrev-ref HEAD 2>/dev/null"),
|
|
26
|
-
"git_dirty" => !sh.call("git status --porcelain 2>/dev/null").empty?,
|
|
27
|
-
"repo_root" => sh.call("git rev-parse --show-toplevel 2>/dev/null"),
|
|
28
|
-
"captured_at" => Time.now.utc.iso8601,
|
|
29
|
-
"os" => RbConfig::CONFIG["host_os"],
|
|
30
|
-
"arch" => RbConfig::CONFIG["host_cpu"],
|
|
31
|
-
"ruby_version" => RUBY_VERSION,
|
|
32
|
-
"runtimes" => { # versions only; nil when not installed
|
|
33
|
-
"node" => ver.call("node --version"),
|
|
34
|
-
"python" => ver.call("python3 --version"),
|
|
35
|
-
"go" => ver.call("go version"),
|
|
36
|
-
},
|
|
37
|
-
"packages" => { # COUNTS only — never the list (size/secrets)
|
|
38
|
-
"brew" => count.call("brew list --formula"), # ~1-3s on macOS; runs only on fetch, amortized by the ttl rule
|
|
39
|
-
"apt" => count.call("dpkg-query -f '.\n' -W"),
|
|
40
|
-
},
|
|
41
|
-
"textus_version" => Textus::VERSION,
|
|
42
|
-
"protocol" => Textus::PROTOCOL,
|
|
43
|
-
} }
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# Reducer that reshapes the raw projection rows into the keys the
|
|
2
|
-
# orientation.mustache template references. Without this, the template
|
|
3
|
-
# would only have access to the flat rows list.
|
|
4
|
-
Textus.hook do |reg|
|
|
5
|
-
reg.on(:transform_rows, :orientation_reducer) do |rows:, **|
|
|
6
|
-
project_row = rows.find { |r| r["_key"] == "knowledge.project" } || {}
|
|
7
|
-
runbook_rows = rows.select { |r| r["_key"]&.start_with?("knowledge.runbooks.") }
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
"project" => {
|
|
11
|
-
"name" => project_row["name"],
|
|
12
|
-
"description" => project_row["description"]
|
|
13
|
-
},
|
|
14
|
-
"runbooks" => runbook_rows.map { |r| { "name" => r["name"], "description" => r["description"] } }
|
|
15
|
-
}
|
|
16
|
-
end
|
|
17
|
-
end
|
data/lib/textus/jobs/handlers.rb
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
3
|
-
# Wires the closed allow-list of convergence job types to the existing
|
|
4
|
-
# convergence code. Authority is read from the job's frozen `enqueued_by`
|
|
5
|
-
# and turned into the Call the handler runs under: produce self-elevates
|
|
6
|
-
# inside Produce::Engine regardless; destructive sweep runs AS the caller.
|
|
7
|
-
module Handlers
|
|
8
|
-
module_function
|
|
9
|
-
|
|
10
|
-
def registry
|
|
11
|
-
reg = Textus::Domain::Jobs::Registry.new
|
|
12
|
-
# produce is pure (self-elevates) — any caller may request a rematerialize.
|
|
13
|
-
reg.register("materialize", handler: method(:produce))
|
|
14
|
-
reg.register("re-pull", handler: method(:produce))
|
|
15
|
-
# sweep is destructive — gate ad-hoc enqueue to the automation authority.
|
|
16
|
-
reg.register("sweep", handler: method(:sweep), required_role: Textus::Role::AUTOMATION)
|
|
17
|
-
reg
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# produce: render derived / re-pull intake for a single key. Engine
|
|
21
|
-
# self-elevates to the build actor internally; the passed call carries
|
|
22
|
-
# only correlation/dry_run plus the stamped role for audit. Engine#call
|
|
23
|
-
# isolates per-key produce errors into its result hash rather than raising,
|
|
24
|
-
# so surface them as :produce_failed events (the converge result hash used
|
|
25
|
-
# to carry them; the worker drops the return, so re-publish here).
|
|
26
|
-
def produce(job:, container:)
|
|
27
|
-
call = call_for(job)
|
|
28
|
-
result = Textus::Produce::Engine.converge(container: container, call: call, keys: [job.args["key"]])
|
|
29
|
-
return unless result.is_a?(Hash)
|
|
30
|
-
|
|
31
|
-
Array(result[:failed]).each do |failure|
|
|
32
|
-
container.events.publish(
|
|
33
|
-
:produce_failed,
|
|
34
|
-
ctx: Textus::Hooks::Context.for(container: container, call: call),
|
|
35
|
-
keys: [failure["key"]], error: failure["error"]
|
|
36
|
-
)
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# sweep: compute retention rows for the scope, then apply destructively AS
|
|
41
|
-
# the job's role (no self-elevation).
|
|
42
|
-
def sweep(job:, container:)
|
|
43
|
-
call = call_for(job)
|
|
44
|
-
scope = job.args["scope"]
|
|
45
|
-
rows = Textus::Domain::Retention::Sweep.new(
|
|
46
|
-
manifest: container.manifest,
|
|
47
|
-
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
48
|
-
clock: Textus::Ports::Clock.new,
|
|
49
|
-
).call(prefix: scope_prefix(scope), zone: scope_zone(scope))
|
|
50
|
-
Textus::Maintenance::Retention::Apply.new(container: container, call: call).call(rows)
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def call_for(job)
|
|
54
|
-
Textus::Call.build(role: job.enqueued_by || Textus::Role::AUTOMATION)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# A scope is `{ "prefix" => ..., "zone" => ... }` or nil (whole store).
|
|
58
|
-
def scope_prefix(scope) = scope.is_a?(Hash) ? scope["prefix"] : nil
|
|
59
|
-
def scope_zone(scope) = scope.is_a?(Hash) ? scope["zone"] : nil
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
3
|
-
# Time-based seeding for the daemon: at each tick, enqueue a re-pull job for
|
|
4
|
-
# every intake key past its source.ttl and a sweep job to GC entries past
|
|
5
|
-
# retention.ttl. Dedup means a job already queued from a prior tick is a
|
|
6
|
-
# no-op. Both are stamped automation (the daemon's own authority); the sweep
|
|
7
|
-
# handler runs retention as that role.
|
|
8
|
-
class Scheduler
|
|
9
|
-
def initialize(container:, queue:)
|
|
10
|
-
@container = container
|
|
11
|
-
@queue = queue
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def run_once
|
|
15
|
-
stale_intake.each do |key|
|
|
16
|
-
@queue.enqueue(job("re-pull", { "key" => key }))
|
|
17
|
-
end
|
|
18
|
-
@queue.enqueue(job("sweep", { "scope" => { "prefix" => nil, "zone" => nil } }))
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def stale_intake
|
|
24
|
-
Textus::Domain::Freshness::Evaluator.new(
|
|
25
|
-
manifest: @container.manifest,
|
|
26
|
-
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
27
|
-
clock: Textus::Ports::Clock.new,
|
|
28
|
-
).stale_intake_keys(prefix: nil, zone: nil)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def job(type, args)
|
|
32
|
-
Textus::Domain::Jobs::Job.new(type: type, args: args, enqueued_by: Textus::Role::AUTOMATION)
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
data/lib/textus/jobs/seeder.rb
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
3
|
-
# Enqueues the full convergence set for a scope: a produce job per derived /
|
|
4
|
-
# publish_tree / publish.to entry, a re-pull job per stale intake key, and a
|
|
5
|
-
# single sweep job for the scope. The scope logic mirrors
|
|
6
|
-
# the converge scope (Produce::Engine) so `drain` and `serve` converge identically.
|
|
7
|
-
# Produce jobs self-elevate (stamped automation); the sweep job carries the
|
|
8
|
-
# caller's role (destructive runs as caller).
|
|
9
|
-
class Seeder
|
|
10
|
-
def initialize(container:, queue:, call:)
|
|
11
|
-
@container = container
|
|
12
|
-
@queue = queue
|
|
13
|
-
@call = call
|
|
14
|
-
@manifest = container.manifest
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def seed(prefix:, zone:)
|
|
18
|
-
file_stat = Textus::Ports::Storage::FileStat.new
|
|
19
|
-
|
|
20
|
-
producible_keys(prefix, zone).each do |key|
|
|
21
|
-
@queue.enqueue(job("materialize", { "key" => key }, Textus::Role::AUTOMATION))
|
|
22
|
-
end
|
|
23
|
-
stale_intake_keys(prefix, zone, file_stat).each do |key|
|
|
24
|
-
@queue.enqueue(job("re-pull", { "key" => key }, Textus::Role::AUTOMATION))
|
|
25
|
-
end
|
|
26
|
-
@queue.enqueue(job("sweep", { "scope" => { "prefix" => prefix, "zone" => zone } }, @call.role))
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def job(type, args, role)
|
|
32
|
-
Textus::Domain::Jobs::Job.new(type: type, args: args, enqueued_by: role)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Mirrors the converge scope (the publishable arm).
|
|
36
|
-
def producible_keys(prefix, zone)
|
|
37
|
-
@manifest.data.entries
|
|
38
|
-
.select { |e| e.derived? || !e.publish_tree.nil? || !e.publish_to.empty? }
|
|
39
|
-
.select { |e| in_scope?(e, prefix, zone) }
|
|
40
|
-
.map(&:key)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def stale_intake_keys(prefix, zone, file_stat)
|
|
44
|
-
Textus::Domain::Freshness::Evaluator.new(
|
|
45
|
-
manifest: @manifest, file_stat: file_stat, clock: Textus::Ports::Clock.new,
|
|
46
|
-
).stale_intake_keys(prefix: prefix, zone: zone)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def in_scope?(entry, prefix, zone)
|
|
50
|
-
return false if zone && entry.zone != zone
|
|
51
|
-
return false if prefix && !entry.key.start_with?(prefix)
|
|
52
|
-
|
|
53
|
-
true
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Maintenance
|
|
3
|
-
# Converge-and-exit: seed the full convergence set for the scope, run the
|
|
4
|
-
# worker until the queue is empty, return a health summary. Exits not-ok if
|
|
5
|
-
# any job dead-lettered. This is the converge entry point and what CI
|
|
6
|
-
# runs. Single-pass (serial) on purpose: each produce job self-locks via
|
|
7
|
-
# Produce::Engine.converge, so running them in turn keeps the build lock
|
|
8
|
-
# uncontended; a concurrent pool would make all-but-one produce job hit
|
|
9
|
-
# BuildInProgress and skip.
|
|
10
|
-
class Drain
|
|
11
|
-
extend Textus::Contract::DSL
|
|
12
|
-
|
|
13
|
-
verb :drain
|
|
14
|
-
summary "Converge everything now: seed produce + retention jobs and drain the queue to empty."
|
|
15
|
-
surfaces :cli, :mcp
|
|
16
|
-
cli "drain"
|
|
17
|
-
arg :prefix, String, description: "restrict convergence to keys under this dotted prefix"
|
|
18
|
-
arg :zone, String, description: "restrict convergence to entries in this zone"
|
|
19
|
-
|
|
20
|
-
def initialize(container:, call:)
|
|
21
|
-
@container = container
|
|
22
|
-
@call = call
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def call(prefix: nil, zone: nil)
|
|
26
|
-
queue = Textus::Ports::Queue.new(root: @container.root)
|
|
27
|
-
Textus::Jobs::Seeder.new(container: @container, queue: queue, call: @call).seed(prefix: prefix, zone: zone)
|
|
28
|
-
|
|
29
|
-
summary = Worker.for(container: @container, queue: queue).drain
|
|
30
|
-
health = Read::Doctor.new(container: @container, call: @call).call
|
|
31
|
-
|
|
32
|
-
{
|
|
33
|
-
"protocol" => Textus::PROTOCOL,
|
|
34
|
-
"ok" => summary.failed.zero?,
|
|
35
|
-
"completed" => summary.completed,
|
|
36
|
-
"failed" => summary.failed,
|
|
37
|
-
"health" => health,
|
|
38
|
-
}
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|