textus 0.52.0 → 0.53.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 +25 -0
- data/README.md +62 -54
- data/SPEC.md +62 -187
- data/docs/architecture/README.md +88 -77
- data/exe/textus +1 -1
- data/lib/textus/action/accept.rb +53 -0
- data/lib/textus/action/audit.rb +133 -0
- data/lib/textus/action/base.rb +42 -0
- data/lib/textus/{read → action}/blame.rb +30 -22
- data/lib/textus/action/boot.rb +26 -0
- data/lib/textus/action/data_mv.rb +71 -0
- data/lib/textus/action/deps.rb +48 -0
- data/lib/textus/action/doctor.rb +26 -0
- data/lib/textus/action/drain.rb +41 -0
- data/lib/textus/action/enqueue.rb +55 -0
- data/lib/textus/action/get.rb +80 -0
- data/lib/textus/action/jobs.rb +38 -0
- data/lib/textus/action/key_delete.rb +46 -0
- data/lib/textus/action/key_delete_prefix.rb +46 -0
- data/lib/textus/action/key_mv.rb +143 -0
- data/lib/textus/action/key_mv_prefix.rb +59 -0
- data/lib/textus/action/list.rb +44 -0
- data/lib/textus/action/propose.rb +54 -0
- data/lib/textus/action/published.rb +26 -0
- data/lib/textus/action/pulse/scanner.rb +118 -0
- data/lib/textus/action/pulse.rb +87 -0
- data/lib/textus/action/put.rb +63 -0
- data/lib/textus/action/rdeps.rb +49 -0
- data/lib/textus/action/reject.rb +49 -0
- data/lib/textus/action/rule_explain.rb +95 -0
- data/lib/textus/action/rule_lint.rb +70 -0
- data/lib/textus/action/rule_list.rb +46 -0
- data/lib/textus/action/schema_envelope.rb +31 -0
- data/lib/textus/action/uid.rb +35 -0
- data/lib/textus/action/where.rb +38 -0
- data/lib/textus/action/write_verb.rb +58 -0
- data/lib/textus/background/job/base.rb +27 -0
- data/lib/textus/background/job/materialize.rb +31 -0
- data/lib/textus/background/job/refresh.rb +22 -0
- data/lib/textus/background/job/sweep.rb +31 -0
- data/lib/textus/background/job.rb +19 -0
- data/lib/textus/background/plan.rb +9 -0
- data/lib/textus/background/planner/plan.rb +113 -0
- data/lib/textus/{maintenance → background}/retention/apply.rb +7 -9
- data/lib/textus/background/worker.rb +67 -0
- data/lib/textus/boot.rb +53 -45
- data/lib/textus/command.rb +36 -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 +11 -11
- 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/handler_permit.rb +34 -0
- data/lib/textus/doctor/check/hooks.rb +11 -18
- data/lib/textus/doctor/check/illegal_keys.rb +1 -1
- data/lib/textus/doctor/check/intake_registration.rb +5 -5
- data/lib/textus/doctor/check/proposal_targets.rb +3 -3
- 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 +12 -9
- data/lib/textus/{read → doctor}/validator.rb +22 -13
- data/lib/textus/doctor.rb +6 -6
- data/lib/textus/envelope/io/writer.rb +65 -36
- data/lib/textus/envelope.rb +5 -3
- data/lib/textus/errors.rb +17 -9
- data/lib/textus/events.rb +21 -0
- data/lib/textus/gate/auth.rb +181 -0
- data/lib/textus/gate.rb +114 -0
- data/lib/textus/init/templates/machine_intake.rb +39 -35
- data/lib/textus/init/templates/orientation_reducer.rb +15 -11
- data/lib/textus/init.rb +90 -73
- data/lib/textus/key/path.rb +9 -2
- data/lib/textus/layout.rb +13 -0
- data/lib/textus/manifest/data.rb +14 -14
- data/lib/textus/manifest/entry/base.rb +15 -11
- data/lib/textus/manifest/entry/parser.rb +6 -6
- data/lib/textus/manifest/entry/produced.rb +3 -2
- data/lib/textus/manifest/entry/publish/mode.rb +1 -1
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -1
- data/lib/textus/manifest/entry/validators/events.rb +1 -1
- data/lib/textus/{domain/policy/handler_allowlist.rb → manifest/policy/handler_permit.rb} +4 -4
- 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/{domain → manifest}/policy/source.rb +24 -19
- data/lib/textus/manifest/policy.rb +36 -48
- data/lib/textus/manifest/resolver.rb +3 -2
- data/lib/textus/manifest/rules.rb +4 -4
- data/lib/textus/manifest/schema/keys.rb +17 -11
- data/lib/textus/manifest/schema/validator.rb +24 -22
- data/lib/textus/manifest/schema/vocabulary.rb +1 -1
- data/lib/textus/manifest/schema.rb +2 -2
- data/lib/textus/manifest.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/handler.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/intake.rb +22 -20
- data/lib/textus/{produce → pipeline}/acquire/projection.rb +13 -11
- data/lib/textus/{produce → pipeline}/acquire/serializer/json.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer/text.rb +1 -1
- data/lib/textus/{produce → pipeline}/acquire/serializer/yaml.rb +2 -2
- data/lib/textus/{produce → pipeline}/acquire/serializer.rb +1 -1
- data/lib/textus/{produce → pipeline}/engine.rb +7 -5
- data/lib/textus/{produce → pipeline}/render.rb +3 -1
- data/lib/textus/ports/audit_log.rb +31 -5
- data/lib/textus/ports/audit_subscriber.rb +4 -4
- data/lib/textus/{domain/jobs → ports/queue}/job.rb +19 -12
- data/lib/textus/ports/queue.rb +1 -1
- data/lib/textus/ports/sentinel_store.rb +2 -2
- data/lib/textus/ports/watcher_lock.rb +48 -0
- data/lib/textus/projection.rb +8 -8
- data/lib/textus/schema/tools.rb +4 -3
- data/lib/textus/session.rb +6 -3
- data/lib/textus/step/base.rb +35 -0
- data/lib/textus/step/builtin/csv_fetch.rb +19 -0
- data/lib/textus/step/builtin/ical_events_fetch.rb +30 -0
- data/lib/textus/step/builtin/json_fetch.rb +18 -0
- data/lib/textus/step/builtin/markdown_links_fetch.rb +20 -0
- data/lib/textus/step/builtin/rss_fetch.rb +26 -0
- data/lib/textus/step/builtin.rb +22 -0
- data/lib/textus/{hooks → step}/catalog.rb +3 -3
- data/lib/textus/{hooks → step}/context.rb +15 -13
- data/lib/textus/step/discovery.rb +24 -0
- data/lib/textus/{hooks → step}/error_log.rb +1 -1
- data/lib/textus/{hooks → step}/event_bus.rb +15 -16
- data/lib/textus/step/fetch.rb +13 -0
- data/lib/textus/{hooks → step}/fire_report.rb +1 -1
- data/lib/textus/step/loader.rb +108 -0
- data/lib/textus/step/observe.rb +31 -0
- data/lib/textus/step/registry_store.rb +66 -0
- data/lib/textus/{hooks → step}/signature.rb +1 -1
- data/lib/textus/step/transform.rb +12 -0
- data/lib/textus/step/validate.rb +11 -0
- data/lib/textus/step.rb +10 -0
- data/lib/textus/store.rb +17 -15
- 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 +34 -0
- data/lib/textus/surfaces/mcp/server.rb +145 -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 +38 -0
- data/lib/textus/surfaces/watcher.rb +38 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus.rb +64 -22
- metadata +132 -118
- 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/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/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/hooks/builtin.rb +0 -70
- data/lib/textus/hooks/loader.rb +0 -54
- data/lib/textus/hooks/rpc_registry.rb +0 -43
- 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/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/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/produce_on_write_subscriber.rb +0 -73
- data/lib/textus/produce/events.rb +0 -36
- 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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class RuleList < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :rule_list
|
|
9
|
+
summary "List every rule block in the manifest."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "rule list"
|
|
12
|
+
view(:cli) { |policies| { "verb" => "rule_list", "policies" => policies } }
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def call(container:, **)
|
|
17
|
+
manifest = container.manifest
|
|
18
|
+
manifest.rules.blocks.map do |block|
|
|
19
|
+
row = { "match" => block.match }
|
|
20
|
+
self.class::LIST_FIELDS.each do |field|
|
|
21
|
+
value = block.public_send(field)
|
|
22
|
+
row[field.to_s] = serialize(field, value) unless value.nil?
|
|
23
|
+
end
|
|
24
|
+
row
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
LIST_FIELDS = Textus::Manifest::Schema::FIELD_REGISTRY.select { |_, m| m[:in_rule_list] }.keys.freeze
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def serialize(field, value)
|
|
33
|
+
case field
|
|
34
|
+
when :retention
|
|
35
|
+
{ "ttl_seconds" => value.ttl_seconds, "action" => value.action.to_s }
|
|
36
|
+
when :react
|
|
37
|
+
value.to_h
|
|
38
|
+
when :handler_permit
|
|
39
|
+
value.handlers
|
|
40
|
+
else
|
|
41
|
+
value
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class SchemaEnvelope < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :schema_show
|
|
9
|
+
summary "Return the schema (field shape) for an entry's family, by key."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
cli "schema show"
|
|
12
|
+
arg :key, String, required: true, positional: true,
|
|
13
|
+
description: "any key in the family whose schema you want; returns required/optional fields and their types"
|
|
14
|
+
|
|
15
|
+
BURN = :sync
|
|
16
|
+
|
|
17
|
+
def initialize(key:)
|
|
18
|
+
super()
|
|
19
|
+
@key = key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(container:, **)
|
|
23
|
+
manifest = container.manifest
|
|
24
|
+
schemas = container.schemas
|
|
25
|
+
mentry = manifest.resolver.resolve(@key).entry
|
|
26
|
+
schema = schemas.fetch_or_nil(mentry.schema)
|
|
27
|
+
{ "protocol" => PROTOCOL, "key" => @key, "schema_ref" => mentry.schema, "schema" => schema&.to_h }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Uid < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :uid
|
|
9
|
+
summary "Return the stable UID of an entry without reading its body."
|
|
10
|
+
surfaces :cli
|
|
11
|
+
cli "key uid"
|
|
12
|
+
arg :key, String, required: true, positional: true, description: "entry key"
|
|
13
|
+
view(:cli) { |uid, inputs| { "key" => inputs[:key], "uid" => uid } }
|
|
14
|
+
|
|
15
|
+
BURN = :sync
|
|
16
|
+
|
|
17
|
+
def initialize(key:)
|
|
18
|
+
super()
|
|
19
|
+
@key = key
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(container:, call:)
|
|
23
|
+
Textus::Action::Get.new(key: @key).call(container: container, call: call).uid
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.new(*args, **kwargs)
|
|
27
|
+
return super(**kwargs) unless args.any?
|
|
28
|
+
|
|
29
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
30
|
+
mapped = positional.zip(args).to_h
|
|
31
|
+
super(**mapped.merge(kwargs))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class Where < Base
|
|
6
|
+
extend Textus::Contract::DSL
|
|
7
|
+
|
|
8
|
+
verb :where
|
|
9
|
+
summary "Resolve a key to its zone, owner, and path without reading the body."
|
|
10
|
+
surfaces :cli, :mcp
|
|
11
|
+
arg :key, String, required: true, positional: true,
|
|
12
|
+
description: "dotted key to locate (returns zone, owner, path; does not read content)"
|
|
13
|
+
|
|
14
|
+
BURN = :sync
|
|
15
|
+
|
|
16
|
+
def initialize(key:)
|
|
17
|
+
super()
|
|
18
|
+
@key = key
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(container:, call: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
22
|
+
manifest = container.manifest
|
|
23
|
+
res = manifest.resolver.resolve(@key)
|
|
24
|
+
mentry = res.entry
|
|
25
|
+
path = res.path
|
|
26
|
+
{ "protocol" => PROTOCOL, "key" => @key, "lane" => mentry.lane, "owner" => mentry.owner, "path" => path }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.new(*args, **kwargs)
|
|
30
|
+
return super(**kwargs) unless args.any?
|
|
31
|
+
|
|
32
|
+
positional = instance_method(:initialize).parameters.slice(:keyreq, :key).map(&:last)
|
|
33
|
+
mapped = positional.zip(args).to_h
|
|
34
|
+
super(**mapped.merge(kwargs))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Action
|
|
5
|
+
class WriteVerb < Base
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def auth(container)
|
|
9
|
+
Textus::Gate::Auth.new(container)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def writer(container, call)
|
|
13
|
+
Textus::Envelope::IO::Writer.from(container: container, call: call)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def reader(container)
|
|
17
|
+
Textus::Envelope::IO::Reader.from(container: container)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run_with_cascade(target_key, container:, call:)
|
|
21
|
+
result = yield
|
|
22
|
+
cascade_to_rdeps(target_key, container, call) if target_key
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def cascade_to_rdeps(key, container, call)
|
|
27
|
+
return if derived_write?(key, container)
|
|
28
|
+
|
|
29
|
+
rdeps = Textus::Action::Rdeps.new(key: key).call(container: container, call: call).fetch("rdeps", [])
|
|
30
|
+
producible = rdeps.select { |dep_key| producible?(dep_key, container) }
|
|
31
|
+
return if producible.empty?
|
|
32
|
+
|
|
33
|
+
producible.each do |dep_key|
|
|
34
|
+
Textus::Background::Job::Materialize.new(key: dep_key).call(container:, call:)
|
|
35
|
+
end
|
|
36
|
+
container.steps.publish(
|
|
37
|
+
:entry_written,
|
|
38
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
39
|
+
key: key,
|
|
40
|
+
envelope: nil,
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def derived_write?(key, container)
|
|
45
|
+
container.manifest.resolver.resolve(key).entry.derived?
|
|
46
|
+
rescue Textus::Error
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def producible?(key, container)
|
|
51
|
+
entry = container.manifest.resolver.resolve(key).entry
|
|
52
|
+
entry.derived? || !entry.publish_tree.nil?
|
|
53
|
+
rescue Textus::Error
|
|
54
|
+
false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
class Base
|
|
7
|
+
def self.inherited(subclass)
|
|
8
|
+
super
|
|
9
|
+
return unless subclass.name
|
|
10
|
+
|
|
11
|
+
TracePoint.new(:end) do |tp|
|
|
12
|
+
if tp.self == subclass
|
|
13
|
+
Textus::Background::Job.register(subclass)
|
|
14
|
+
tp.disable
|
|
15
|
+
end
|
|
16
|
+
end.enable
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def call(**)
|
|
20
|
+
raise NotImplementedError.new("#{self.class}#call")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def args = {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
class Materialize < Base
|
|
7
|
+
TYPE = "materialize"
|
|
8
|
+
|
|
9
|
+
def initialize(key:)
|
|
10
|
+
super()
|
|
11
|
+
@key = key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def args = { key: @key }
|
|
15
|
+
|
|
16
|
+
def call(container:, call:)
|
|
17
|
+
result = Textus::Pipeline::Engine.converge(container: container, call: call, keys: [@key])
|
|
18
|
+
return unless result.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
Array(result[:failed]).each do |failure|
|
|
21
|
+
container.steps.publish(
|
|
22
|
+
:produce_failed,
|
|
23
|
+
ctx: Textus::Step::Context.for(container: container, call: call),
|
|
24
|
+
keys: [failure["key"]], error: failure["error"]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
class Refresh < Base
|
|
7
|
+
TYPE = "refresh"
|
|
8
|
+
|
|
9
|
+
def initialize(key:)
|
|
10
|
+
super()
|
|
11
|
+
@key = key
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def args = { key: @key }
|
|
15
|
+
|
|
16
|
+
def call(container:, call:)
|
|
17
|
+
Textus::Pipeline::Engine.converge(container: container, call: call, keys: [@key])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
class Sweep < Base
|
|
7
|
+
REQUIRED_ROLE = Textus::Role::AUTOMATION
|
|
8
|
+
TYPE = "sweep"
|
|
9
|
+
|
|
10
|
+
def initialize(scope: nil, key: nil)
|
|
11
|
+
super()
|
|
12
|
+
@scope = scope || {}
|
|
13
|
+
@key = key
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def args = { scope: @scope, key: @key }.compact
|
|
17
|
+
|
|
18
|
+
def call(container:, call:)
|
|
19
|
+
prefix = @key || (@scope.is_a?(Hash) ? @scope["prefix"] : nil)
|
|
20
|
+
lane = @scope.is_a?(Hash) ? @scope["lane"] : nil
|
|
21
|
+
rows = Textus::Core::Retention::Sweep.new(
|
|
22
|
+
manifest: container.manifest,
|
|
23
|
+
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
24
|
+
clock: Textus::Ports::Clock.new,
|
|
25
|
+
).call(prefix: prefix, lane: lane)
|
|
26
|
+
Textus::Background::Retention::Apply.new(container: container, call: call).call(rows)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Job
|
|
6
|
+
@registry = {}
|
|
7
|
+
|
|
8
|
+
def self.registry = @registry
|
|
9
|
+
|
|
10
|
+
def self.register(klass)
|
|
11
|
+
@registry[klass::TYPE] = klass if klass.const_defined?(:TYPE, false)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.fetch(type)
|
|
15
|
+
@registry.fetch(type) { raise Textus::UsageError.new("unknown job type: #{type}") }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Background
|
|
5
|
+
module Planner
|
|
6
|
+
class Plan
|
|
7
|
+
ACTIONS_BY_TRIGGER = {
|
|
8
|
+
"convergence" => %w[materialize refresh sweep],
|
|
9
|
+
"entry.written" => %w[materialize],
|
|
10
|
+
"entry.deleted" => %w[materialize],
|
|
11
|
+
"entry.moved" => %w[materialize],
|
|
12
|
+
"proposal.accepted" => %w[materialize],
|
|
13
|
+
"proposal.rejected" => %w[materialize],
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
SCOPE_RESOLVERS = {
|
|
17
|
+
"materialize" => :producible_keys,
|
|
18
|
+
"refresh" => :stale_intake_keys,
|
|
19
|
+
"sweep" => :lane_keys,
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
def self.seed(container:, queue:, role:)
|
|
23
|
+
jobs = new(container: container).plan(
|
|
24
|
+
trigger: { "type" => "convergence" },
|
|
25
|
+
role: role,
|
|
26
|
+
)
|
|
27
|
+
jobs.each { |j| queue.enqueue(j) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize(container:)
|
|
31
|
+
@container = container
|
|
32
|
+
@manifest = container.manifest
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def plan(trigger:, role:)
|
|
36
|
+
type = trigger["type"] || trigger[:type]
|
|
37
|
+
trigger["target"] || trigger[:target]
|
|
38
|
+
return [] if type.nil?
|
|
39
|
+
|
|
40
|
+
blocks_with_react = @manifest.rules.blocks.select(&:react)
|
|
41
|
+
if blocks_with_react.any?
|
|
42
|
+
plan_from_rules(blocks_with_react, type, role)
|
|
43
|
+
else
|
|
44
|
+
plan_from_defaults(type, role)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def plan_from_rules(blocks, type, role)
|
|
51
|
+
jobs = []
|
|
52
|
+
blocks
|
|
53
|
+
.select { |b| matches_trigger?(b.react, type) }
|
|
54
|
+
.each do |block|
|
|
55
|
+
do_action = block.react.raw["do"]
|
|
56
|
+
Array(do_action).each do |action|
|
|
57
|
+
if action == "sweep"
|
|
58
|
+
jobs << Textus::Ports::Queue::Job.new(
|
|
59
|
+
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
60
|
+
)
|
|
61
|
+
else
|
|
62
|
+
resolver = SCOPE_RESOLVERS.fetch(action, :producible_keys)
|
|
63
|
+
keys = send(resolver, nil)
|
|
64
|
+
keys.each { |key| jobs << job(action, key, role) }
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
jobs
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def plan_from_defaults(type, role)
|
|
72
|
+
actions = ACTIONS_BY_TRIGGER.fetch(type, [])
|
|
73
|
+
jobs = []
|
|
74
|
+
producible_keys(nil).each { |k| jobs << job("materialize", k, role) } if actions.include?("materialize")
|
|
75
|
+
stale_intake_keys(nil).each { |k| jobs << job("refresh", k, role) } if actions.include?("refresh")
|
|
76
|
+
if actions.include?("sweep")
|
|
77
|
+
jobs << Textus::Ports::Queue::Job.new(
|
|
78
|
+
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
jobs
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def matches_trigger?(react, type)
|
|
85
|
+
on = react.raw["on"]
|
|
86
|
+
Array(on).include?(type)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def job(type, key, enqueued_by)
|
|
90
|
+
Textus::Ports::Queue::Job.new(type: type, args: { "key" => key }, enqueued_by: enqueued_by)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def producible_keys(_target)
|
|
94
|
+
@manifest.data.entries
|
|
95
|
+
.select { |e| e.derived? || !e.publish_tree.nil? || !e.publish_to.empty? }
|
|
96
|
+
.map(&:key)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def stale_intake_keys(_target)
|
|
100
|
+
Textus::Core::Freshness::Evaluator.new(
|
|
101
|
+
manifest: @manifest,
|
|
102
|
+
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
103
|
+
clock: Textus::Ports::Clock.new,
|
|
104
|
+
).stale_intake_keys(prefix: nil, lane: nil)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def lane_keys(_target)
|
|
108
|
+
@manifest.data.entries.map(&:key)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
require "fileutils"
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
|
-
module
|
|
4
|
+
module Background
|
|
5
5
|
module Retention
|
|
6
|
-
# The destructive half of convergence: apply retention rows (drop/archive).
|
|
7
|
-
# Lifted verbatim from the legacy reconcile apply/archive_leaf so drain/serve and
|
|
8
|
-
# the `sweep` job handler share one path. Runs as the caller's role — never
|
|
9
|
-
# self-elevates (ADR 0079/0093: destructiveness decides authority).
|
|
10
6
|
class Apply
|
|
11
7
|
def initialize(container:, call:)
|
|
12
8
|
@container = container
|
|
@@ -15,17 +11,16 @@ module Textus
|
|
|
15
11
|
|
|
16
12
|
def call(rows)
|
|
17
13
|
out = { dropped: [], archived: [], failed: [] }
|
|
18
|
-
delete = Write::KeyDelete.new(container: @container, call: @call)
|
|
19
14
|
rows.each do |row|
|
|
20
15
|
key = row["key"]
|
|
21
16
|
begin
|
|
22
17
|
case row["action"]
|
|
23
18
|
when "drop"
|
|
24
|
-
delete
|
|
19
|
+
delete(key)
|
|
25
20
|
out[:dropped] << key
|
|
26
21
|
when "archive"
|
|
27
22
|
archive_leaf(row)
|
|
28
|
-
delete
|
|
23
|
+
delete(key)
|
|
29
24
|
out[:archived] << key
|
|
30
25
|
end
|
|
31
26
|
rescue Textus::Error => e
|
|
@@ -37,7 +32,6 @@ module Textus
|
|
|
37
32
|
|
|
38
33
|
private
|
|
39
34
|
|
|
40
|
-
# Copy the leaf into <store>/archive/<relative-path> before deletion.
|
|
41
35
|
def archive_leaf(row)
|
|
42
36
|
src = row["path"]
|
|
43
37
|
root = @container.root.to_s
|
|
@@ -46,6 +40,10 @@ module Textus
|
|
|
46
40
|
FileUtils.mkdir_p(File.dirname(dest))
|
|
47
41
|
FileUtils.cp(src, dest)
|
|
48
42
|
end
|
|
43
|
+
|
|
44
|
+
def delete(key)
|
|
45
|
+
Textus::Action::KeyDelete.new(key: key).call(container: @container, call: @call)
|
|
46
|
+
end
|
|
49
47
|
end
|
|
50
48
|
end
|
|
51
49
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Textus
|
|
2
|
+
module Background
|
|
3
|
+
class Worker
|
|
4
|
+
Summary = Struct.new(:completed, :failed, keyword_init: true)
|
|
5
|
+
|
|
6
|
+
def self.for(container:, queue:)
|
|
7
|
+
new(queue: queue, container: container,
|
|
8
|
+
lease_ttl: container.manifest.data.worker_config[:lease_ttl])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def initialize(queue:, container:, lease_ttl: 60)
|
|
12
|
+
@queue = queue
|
|
13
|
+
@container = container
|
|
14
|
+
@lease_ttl = lease_ttl
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def drain(worker_id: "drain-#{Process.pid}")
|
|
18
|
+
completed = 0
|
|
19
|
+
failed = 0
|
|
20
|
+
loop do
|
|
21
|
+
leased = @queue.lease(worker_id: worker_id, lease_ttl: @lease_ttl)
|
|
22
|
+
break unless leased
|
|
23
|
+
|
|
24
|
+
case run_one(leased)
|
|
25
|
+
when :completed then completed += 1
|
|
26
|
+
when :dead_lettered then failed += 1
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
Summary.new(completed: completed, failed: failed)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def drain_pool(pool: 4)
|
|
33
|
+
summaries = []
|
|
34
|
+
mutex = Mutex.new
|
|
35
|
+
threads = Array.new(pool) do |i|
|
|
36
|
+
Thread.new do
|
|
37
|
+
s = drain(worker_id: "pool-#{Process.pid}-#{i}")
|
|
38
|
+
mutex.synchronize { summaries << s }
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
threads.each(&:join)
|
|
42
|
+
Summary.new(completed: summaries.sum(&:completed), failed: summaries.sum(&:failed))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def run_one(leased)
|
|
48
|
+
job = leased.job
|
|
49
|
+
klass = Textus::Background::Job.fetch(job.type)
|
|
50
|
+
action = if klass.instance_method(:initialize).parameters.any?
|
|
51
|
+
klass.new(**job.args.transform_keys(&:to_sym))
|
|
52
|
+
else
|
|
53
|
+
klass.new
|
|
54
|
+
end
|
|
55
|
+
call = Textus::Call.build(
|
|
56
|
+
role: job.enqueued_by || Textus::Role::AUTOMATION,
|
|
57
|
+
correlation_id: SecureRandom.uuid,
|
|
58
|
+
)
|
|
59
|
+
action.call(container: @container, call: call)
|
|
60
|
+
@queue.ack(leased)
|
|
61
|
+
:completed
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
@queue.fail(leased, error: e.message)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|