textus 0.54.2 → 0.55.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 +38 -1
- data/README.md +17 -10
- data/SPEC.md +39 -11
- data/docs/architecture/README.md +23 -11
- data/docs/reference/conventions.md +5 -2
- data/exe/textus +1 -1
- data/lib/textus/boot.rb +64 -93
- data/lib/textus/dispatch/binder.rb +48 -0
- data/lib/textus/dispatch/contracts.rb +63 -0
- data/lib/textus/dispatch/handler_registry.rb +21 -0
- data/lib/textus/dispatch/middleware/audit_index.rb +51 -0
- data/lib/textus/dispatch/middleware/auth.rb +40 -0
- data/lib/textus/dispatch/middleware/base.rb +26 -0
- data/lib/textus/dispatch/middleware/binder.rb +20 -0
- data/lib/textus/dispatch/middleware/cascade.rb +53 -0
- data/lib/textus/dispatch/pipeline.rb +35 -0
- data/lib/textus/doctor/check/audit_log.rb +2 -2
- data/lib/textus/doctor/check/generator_drift.rb +4 -4
- data/lib/textus/doctor/check/orphaned_publish_targets.rb +3 -3
- data/lib/textus/doctor/check/protocol_version.rb +2 -2
- data/lib/textus/doctor/check/raw_asset_paths.rb +1 -1
- data/lib/textus/doctor/check/schema_parse_error.rb +1 -1
- data/lib/textus/doctor/check/schema_violations.rb +2 -2
- data/lib/textus/doctor/check/schemas.rb +1 -1
- data/lib/textus/doctor/check/{notebook_sources.rb → scratchpad_sources.rb} +10 -5
- data/lib/textus/doctor/check/sentinels.rb +4 -4
- data/lib/textus/doctor/check/templates.rb +1 -1
- data/lib/textus/doctor/check/unowned_schema_fields.rb +1 -1
- data/lib/textus/doctor/check.rb +9 -10
- data/lib/textus/doctor.rb +2 -2
- data/lib/textus/errors.rb +8 -0
- data/lib/textus/format/base.rb +34 -10
- data/lib/textus/format/json.rb +5 -27
- data/lib/textus/format/markdown.rb +5 -27
- data/lib/textus/format/text.rb +5 -4
- data/lib/textus/format/yaml.rb +30 -27
- data/lib/textus/format.rb +22 -1
- data/lib/textus/handlers/maintenance/boot_store.rb +15 -0
- data/lib/textus/handlers/maintenance/doctor_store.rb +15 -0
- data/lib/textus/handlers/maintenance/drain_store.rb +21 -0
- data/lib/textus/handlers/maintenance/ingest_entry.rb +159 -0
- data/lib/textus/handlers/maintenance/jobs_action.rb +21 -0
- data/lib/textus/handlers/maintenance/published_entries.rb +17 -0
- data/lib/textus/handlers/maintenance/rule_explain.rb +77 -0
- data/lib/textus/handlers/maintenance/rule_lint.rb +54 -0
- data/lib/textus/handlers/maintenance/rule_list.rb +32 -0
- data/lib/textus/handlers/maintenance/schema_envelope.rb +19 -0
- data/lib/textus/handlers/read/audit_entries.rb +48 -0
- data/lib/textus/handlers/read/blame_entry.rb +71 -0
- data/lib/textus/handlers/read/deps_entry.rb +17 -0
- data/lib/textus/handlers/read/get_entry.rb +68 -0
- data/lib/textus/handlers/read/list_keys.rb +36 -0
- data/lib/textus/handlers/read/pulse_entries.rb +66 -0
- data/lib/textus/handlers/read/rdeps_entry.rb +21 -0
- data/lib/textus/handlers/read/uid_entry.rb +18 -0
- data/lib/textus/handlers/read/where_entry.rb +18 -0
- data/lib/textus/handlers/write/accept_proposal.rb +39 -0
- data/lib/textus/handlers/write/data_mv.rb +55 -0
- data/lib/textus/handlers/write/delete_key.rb +17 -0
- data/lib/textus/handlers/write/enqueue_job.rb +27 -0
- data/lib/textus/handlers/write/key_delete_prefix.rb +32 -0
- data/lib/textus/handlers/write/key_mv_prefix.rb +45 -0
- data/lib/textus/handlers/write/move_key.rb +80 -0
- data/lib/textus/handlers/write/propose_entry.rb +29 -0
- data/lib/textus/handlers/write/put_entry.rb +29 -0
- data/lib/textus/handlers/write/reject_proposal.rb +29 -0
- data/lib/textus/init.rb +5 -7
- data/lib/textus/jobs.rb +3 -9
- data/lib/textus/manifest/capabilities.rb +4 -4
- data/lib/textus/manifest/entry/base.rb +3 -3
- data/lib/textus/manifest/entry/publish/subtree_mirror.rb +2 -2
- data/lib/textus/manifest/entry/publish/to_paths.rb +2 -2
- data/lib/textus/manifest/policy/predicates/author_held.rb +22 -0
- data/lib/textus/manifest/policy/predicates/etag_match.rb +18 -0
- data/lib/textus/manifest/policy/predicates/fresh_within.rb +13 -0
- data/lib/textus/manifest/policy/predicates/lane_deletable_by.rb +31 -0
- data/lib/textus/manifest/policy/predicates/lane_writable_by.rb +23 -0
- data/lib/textus/manifest/policy/predicates/raw_lane_ingest_only.rb +25 -0
- data/lib/textus/manifest/policy/predicates/raw_write_once.rb +24 -0
- data/lib/textus/manifest/policy/predicates/schema_valid.rb +41 -0
- data/lib/textus/manifest/policy/predicates/target_is_canon.rb +20 -0
- data/lib/textus/manifest/policy/predicates.rb +54 -0
- data/lib/textus/manifest/policy/retention.rb +1 -1
- data/lib/textus/manifest/schema/semantics/cross_field.rb +53 -0
- data/lib/textus/manifest/schema/semantics/invariants.rb +125 -0
- data/lib/textus/manifest/schema/semantics/migration.rb +83 -0
- data/lib/textus/manifest/schema/semantics.rb +11 -216
- data/lib/textus/orchestration.rb +55 -0
- data/lib/textus/{ports → port}/audit_log.rb +46 -6
- data/lib/textus/{ports → port}/build_lock.rb +2 -2
- data/lib/textus/{ports → port}/clock.rb +1 -1
- data/lib/textus/{ports → port}/publisher.rb +5 -5
- data/lib/textus/{core → port}/sentinel.rb +1 -6
- data/lib/textus/{ports → port}/sentinel_store.rb +4 -4
- data/lib/textus/{ports → port}/storage/file_stat.rb +1 -1
- data/lib/textus/port/storage/file_store.rb +49 -0
- data/lib/textus/port/storage/interface.rb +17 -0
- data/lib/textus/port/store.rb +149 -0
- data/lib/textus/{ports → port}/watcher_lock.rb +3 -3
- data/lib/textus/produce/engine.rb +1 -11
- data/lib/textus/produce/publisher.rb +21 -0
- data/lib/textus/schema/registry.rb +42 -0
- data/lib/textus/schema/tools.rb +7 -10
- data/lib/textus/store/container.rb +173 -0
- data/lib/textus/store/cursor.rb +26 -0
- data/lib/textus/store/entry/reader.rb +47 -0
- data/lib/textus/store/entry/writer.rb +219 -0
- data/lib/textus/store/envelope/meta.rb +61 -0
- data/lib/textus/store/freshness/drift_detector.rb +93 -0
- data/lib/textus/store/freshness/evaluator.rb +20 -0
- data/lib/textus/store/freshness/ttl_evaluator.rb +57 -0
- data/lib/textus/{core → store}/freshness/verdict.rb +1 -11
- data/lib/textus/store/freshness.rb +8 -0
- data/lib/textus/store/index/builder.rb +76 -0
- data/lib/textus/store/index/lookup.rb +60 -0
- data/lib/textus/store/jobs/base.rb +13 -0
- data/lib/textus/store/jobs/index.rb +15 -0
- data/lib/textus/store/jobs/materialize.rb +15 -0
- data/lib/textus/store/jobs/plan.rb +11 -0
- data/lib/textus/store/jobs/planner.rb +124 -0
- data/lib/textus/store/jobs/queue.rb +162 -0
- data/lib/textus/store/jobs/registry.rb +19 -0
- data/lib/textus/store/jobs/retention/base.rb +52 -0
- data/lib/textus/store/jobs/retention/sweep.rb +55 -0
- data/lib/textus/store/jobs/retention.rb +8 -0
- data/lib/textus/store/jobs/sweep.rb +21 -0
- data/lib/textus/store/jobs/worker.rb +64 -0
- data/lib/textus/store/layout.rb +97 -0
- data/lib/textus/store.rb +63 -32
- data/lib/textus/{surfaces → surface}/cli/group/data.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/key.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/mcp.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/rule.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group/schema.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/group.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/runner.rb +14 -68
- data/lib/textus/surface/cli/sources.rb +41 -0
- data/lib/textus/{surfaces → surface}/cli/verb/doctor.rb +5 -6
- data/lib/textus/{surfaces → surface}/cli/verb/get.rb +7 -5
- data/lib/textus/{surfaces → surface}/cli/verb/init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/mcp_serve.rb +3 -3
- data/lib/textus/surface/cli/verb/put.rb +27 -0
- data/lib/textus/{surfaces → surface}/cli/verb/schema_diff.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_init.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/schema_migrate.rb +1 -1
- data/lib/textus/{surfaces → surface}/cli/verb/watch.rb +2 -2
- data/lib/textus/{surfaces → surface}/cli/verb.rb +3 -8
- data/lib/textus/{surfaces → surface}/cli.rb +1 -1
- data/lib/textus/surface/mcp/catalog.rb +58 -0
- data/lib/textus/{surfaces → surface}/mcp/errors.rb +1 -11
- data/lib/textus/surface/mcp/projector.rb +20 -0
- data/lib/textus/{surfaces → surface}/mcp/server.rb +24 -35
- data/lib/textus/{surfaces → surface}/mcp.rb +2 -2
- data/lib/textus/{surfaces → surface}/watcher.rb +8 -8
- data/lib/textus/value/call.rb +30 -0
- data/lib/textus/value/command.rb +16 -0
- data/lib/textus/{core → value}/duration.rb +1 -4
- data/lib/textus/value/envelope.rb +90 -0
- data/lib/textus/value/etag.rb +39 -0
- data/lib/textus/value/payload.rb +7 -0
- data/lib/textus/value/result.rb +46 -0
- data/lib/textus/value/role.rb +38 -0
- data/lib/textus/value/types.rb +13 -0
- data/lib/textus/{uid.rb → value/uid.rb} +9 -7
- data/lib/textus/verb_registry.rb +417 -0
- data/lib/textus/version.rb +1 -1
- data/lib/textus/workflow/loader.rb +4 -4
- data/lib/textus/workflow/runner.rb +11 -33
- data/lib/textus.rb +4 -69
- metadata +142 -105
- data/lib/textus/action/accept.rb +0 -44
- data/lib/textus/action/audit.rb +0 -131
- data/lib/textus/action/base.rb +0 -42
- data/lib/textus/action/blame.rb +0 -97
- data/lib/textus/action/boot.rb +0 -17
- data/lib/textus/action/data_mv.rb +0 -69
- data/lib/textus/action/deps.rb +0 -34
- data/lib/textus/action/doctor.rb +0 -24
- data/lib/textus/action/drain.rb +0 -39
- data/lib/textus/action/enqueue.rb +0 -53
- data/lib/textus/action/get.rb +0 -78
- data/lib/textus/action/ingest.rb +0 -203
- data/lib/textus/action/jobs.rb +0 -36
- data/lib/textus/action/key_delete.rb +0 -36
- data/lib/textus/action/key_delete_prefix.rb +0 -44
- data/lib/textus/action/key_mv.rb +0 -129
- data/lib/textus/action/key_mv_prefix.rb +0 -57
- data/lib/textus/action/list.rb +0 -42
- data/lib/textus/action/propose.rb +0 -52
- data/lib/textus/action/published.rb +0 -24
- data/lib/textus/action/pulse.rb +0 -56
- data/lib/textus/action/put.rb +0 -52
- data/lib/textus/action/rdeps.rb +0 -39
- data/lib/textus/action/reject.rb +0 -37
- data/lib/textus/action/rule_explain.rb +0 -91
- data/lib/textus/action/rule_lint.rb +0 -68
- data/lib/textus/action/rule_list.rb +0 -42
- data/lib/textus/action/schema_envelope.rb +0 -29
- data/lib/textus/action/uid.rb +0 -33
- data/lib/textus/action/where.rb +0 -36
- data/lib/textus/action/write_verb.rb +0 -44
- data/lib/textus/call.rb +0 -28
- data/lib/textus/command.rb +0 -41
- data/lib/textus/container.rb +0 -26
- data/lib/textus/contract/around.rb +0 -29
- data/lib/textus/contract/binder.rb +0 -88
- data/lib/textus/contract/resources/build_lock.rb +0 -17
- data/lib/textus/contract/resources/cursor.rb +0 -26
- data/lib/textus/contract/sources.rb +0 -39
- data/lib/textus/contract/view.rb +0 -15
- data/lib/textus/contract.rb +0 -174
- data/lib/textus/core/freshness/evaluator.rb +0 -150
- data/lib/textus/core/freshness.rb +0 -11
- data/lib/textus/core/retention/sweep.rb +0 -57
- data/lib/textus/core/retention.rb +0 -11
- data/lib/textus/cursor_store.rb +0 -24
- data/lib/textus/envelope/reader.rb +0 -46
- data/lib/textus/envelope/writer.rb +0 -209
- data/lib/textus/envelope.rb +0 -79
- data/lib/textus/etag.rb +0 -36
- data/lib/textus/gate/auth.rb +0 -227
- data/lib/textus/gate.rb +0 -116
- data/lib/textus/jobs/base.rb +0 -23
- data/lib/textus/jobs/materialize.rb +0 -20
- data/lib/textus/jobs/plan.rb +0 -9
- data/lib/textus/jobs/planner.rb +0 -101
- data/lib/textus/jobs/retention.rb +0 -48
- data/lib/textus/jobs/sweep.rb +0 -27
- data/lib/textus/jobs/worker.rb +0 -67
- data/lib/textus/layout.rb +0 -91
- data/lib/textus/ports/job_store/job.rb +0 -65
- data/lib/textus/ports/job_store.rb +0 -123
- data/lib/textus/ports/raw_index.rb +0 -61
- data/lib/textus/ports/storage/file_store.rb +0 -26
- data/lib/textus/role.rb +0 -36
- data/lib/textus/schemas.rb +0 -54
- data/lib/textus/session.rb +0 -35
- data/lib/textus/surfaces/cli/verb/put.rb +0 -30
- data/lib/textus/surfaces/mcp/catalog.rb +0 -111
- data/lib/textus/surfaces/role_scope.rb +0 -34
- data/lib/textus/types.rb +0 -15
data/lib/textus/gate/auth.rb
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Gate
|
|
5
|
-
class Auth
|
|
6
|
-
FLOOR = {
|
|
7
|
-
put: %w[lane_writable_by raw_lane_ingest_only],
|
|
8
|
-
key_delete: %w[lane_deletable_by],
|
|
9
|
-
key_mv: %w[lane_writable_by raw_lane_ingest_only],
|
|
10
|
-
accept: %w[author_held],
|
|
11
|
-
reject: %w[author_held],
|
|
12
|
-
propose: %w[lane_writable_by raw_lane_ingest_only],
|
|
13
|
-
key_mv_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
14
|
-
key_delete_prefix: %w[lane_writable_by raw_lane_ingest_only],
|
|
15
|
-
ingest: %w[lane_writable_by raw_write_once],
|
|
16
|
-
}.freeze
|
|
17
|
-
|
|
18
|
-
AuthContext = Struct.new(
|
|
19
|
-
:actor, :actor_caps, :lane_verb,
|
|
20
|
-
:action, :target, :envelope,
|
|
21
|
-
:mentry, :manifest,
|
|
22
|
-
keyword_init: true
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
def initialize(container)
|
|
26
|
-
@manifest = container.manifest
|
|
27
|
-
@schemas = container.schemas
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Command-based check (new Gate path).
|
|
31
|
-
def check!(cmd)
|
|
32
|
-
key = extract_key(cmd)
|
|
33
|
-
return unless key
|
|
34
|
-
|
|
35
|
-
evaluate_predicates(
|
|
36
|
-
action: command_to_action(cmd),
|
|
37
|
-
actor: cmd.role.to_s,
|
|
38
|
-
key: key,
|
|
39
|
-
envelope: nil,
|
|
40
|
-
extra: {},
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Backward-compatible check for inline action auth (accept, put, etc.).
|
|
45
|
-
def check_action!(action:, actor:, key:, envelope: nil, extra: {})
|
|
46
|
-
evaluate_predicates(
|
|
47
|
-
action: action.to_sym,
|
|
48
|
-
actor: actor,
|
|
49
|
-
key: key,
|
|
50
|
-
envelope: envelope,
|
|
51
|
-
extra: extra,
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def self.command_to_verb
|
|
56
|
-
@command_to_verb ||= Textus::Gate::VERB_COMMAND.invert.freeze
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
private
|
|
60
|
-
|
|
61
|
-
def command_to_action(cmd)
|
|
62
|
-
self.class.command_to_verb.fetch(cmd.class) do
|
|
63
|
-
raise Textus::UsageError.new("unmapped command: #{cmd.class}")
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def evaluate_predicates(action:, actor:, key:, envelope:, extra:)
|
|
68
|
-
mentry = @manifest.resolver.resolve(key).entry
|
|
69
|
-
lane_verb = @manifest.policy.verb_for_lane(mentry.lane.to_s)
|
|
70
|
-
actor_caps = Set.new(@manifest.data.role_caps.fetch(actor, []))
|
|
71
|
-
|
|
72
|
-
ctx = AuthContext.new(
|
|
73
|
-
actor:, actor_caps:, lane_verb:,
|
|
74
|
-
action:, target: key, envelope:,
|
|
75
|
-
mentry:, manifest: @manifest
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
failures = []
|
|
79
|
-
floor_preds = FLOOR.fetch(action, [])
|
|
80
|
-
rule_preds = rule_declared_predicates(action, key)
|
|
81
|
-
(floor_preds + rule_preds).uniq.each do |pred|
|
|
82
|
-
result = evaluate(pred, ctx, extra)
|
|
83
|
-
next if result[:pass]
|
|
84
|
-
raise result[:error] if result[:error]
|
|
85
|
-
|
|
86
|
-
failures << [pred, result[:reason]]
|
|
87
|
-
end
|
|
88
|
-
raise Textus::GuardFailed.new(failures) unless failures.empty?
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def extract_key(cmd)
|
|
92
|
-
if cmd.respond_to?(:pending_key)
|
|
93
|
-
cmd.pending_key
|
|
94
|
-
elsif cmd.respond_to?(:key)
|
|
95
|
-
cmd.key
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def rule_declared_predicates(action, key)
|
|
100
|
-
guard_map = @manifest.rules.for(key).guard
|
|
101
|
-
return [] if guard_map.nil?
|
|
102
|
-
|
|
103
|
-
Array(guard_map[action.to_s])
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def evaluate(pred_name, ctx, extra)
|
|
107
|
-
case pred_name
|
|
108
|
-
when "lane_writable_by" then evaluate_lane_writable_by(ctx)
|
|
109
|
-
when "author_held" then evaluate_author_held(ctx)
|
|
110
|
-
when "target_is_canon" then evaluate_target_is_canon(ctx)
|
|
111
|
-
when "etag_match" then evaluate_etag_match(ctx, extra)
|
|
112
|
-
when "schema_valid" then evaluate_schema_valid(ctx)
|
|
113
|
-
when "fresh_within" then { pass: true }
|
|
114
|
-
when "raw_lane_ingest_only" then evaluate_raw_lane_ingest_only(ctx)
|
|
115
|
-
when "raw_write_once" then evaluate_raw_write_once(ctx)
|
|
116
|
-
when "lane_deletable_by" then evaluate_lane_deletable_by(ctx)
|
|
117
|
-
else raise Textus::UsageError.new("unknown predicate '#{pred_name}'")
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def evaluate_lane_writable_by(ctx)
|
|
122
|
-
pass = ctx.actor_caps.include?(ctx.lane_verb.to_s)
|
|
123
|
-
return { pass: true } if pass
|
|
124
|
-
|
|
125
|
-
holders = @manifest.policy.roles_with_capability(ctx.lane_verb.to_s)
|
|
126
|
-
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane, verb: ctx.lane_verb, holders:) }
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def evaluate_author_held(ctx)
|
|
130
|
-
holders = @manifest.policy.roles_with_capability("author")
|
|
131
|
-
pass = holders.include?(ctx.actor.to_s)
|
|
132
|
-
reason = if pass
|
|
133
|
-
nil
|
|
134
|
-
elsif holders.empty?
|
|
135
|
-
"no role holds the 'author' capability; #{ctx.action} is disabled"
|
|
136
|
-
else
|
|
137
|
-
"role '#{ctx.actor}' lacks the 'author' capability (held by: #{holders.join(", ")})"
|
|
138
|
-
end
|
|
139
|
-
{ pass:, reason: }
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def evaluate_target_is_canon(ctx)
|
|
143
|
-
kind = @manifest.policy.declared_kind(ctx.mentry.lane.to_s)
|
|
144
|
-
pass = kind == :canon
|
|
145
|
-
{ pass:, reason: pass ? nil : "target lane '#{ctx.mentry.lane}' is not canon (kind: #{kind})" }
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def evaluate_etag_match(ctx, extra)
|
|
149
|
-
if_etag = extra[:if_etag]
|
|
150
|
-
return { pass: true } if if_etag.nil?
|
|
151
|
-
|
|
152
|
-
current = ctx.envelope&.etag
|
|
153
|
-
pass = current.nil? || current == if_etag
|
|
154
|
-
{ pass:, error: pass ? nil : Textus::EtagMismatch.new(ctx.target, if_etag, current) }
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def evaluate_schema_valid(ctx)
|
|
158
|
-
return { pass: true } unless ctx.envelope
|
|
159
|
-
|
|
160
|
-
schema_ref = ctx.mentry.schema
|
|
161
|
-
return { pass: true } unless schema_ref
|
|
162
|
-
|
|
163
|
-
schema = @schemas.fetch_or_nil(schema_ref)
|
|
164
|
-
return { pass: true } unless schema
|
|
165
|
-
|
|
166
|
-
frontmatter = ctx.envelope.meta&.dig("_meta") || ctx.envelope.meta || {}
|
|
167
|
-
begin
|
|
168
|
-
schema.validate!(frontmatter)
|
|
169
|
-
{ pass: true }
|
|
170
|
-
rescue Textus::SchemaViolation => e
|
|
171
|
-
{ pass: false, reason: schema_reason(e) }
|
|
172
|
-
end
|
|
173
|
-
end
|
|
174
|
-
|
|
175
|
-
def evaluate_raw_lane_ingest_only(ctx)
|
|
176
|
-
return { pass: true } unless @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
177
|
-
return { pass: true } if ctx.action == :ingest
|
|
178
|
-
|
|
179
|
-
{ pass: false, error: Textus::Error.new(
|
|
180
|
-
:raw_lane_ingest_only,
|
|
181
|
-
"raw lane '#{ctx.mentry.lane}' only accepts `textus ingest` — " \
|
|
182
|
-
"use that verb instead of '#{ctx.action}'",
|
|
183
|
-
) }
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
def evaluate_raw_write_once(ctx)
|
|
187
|
-
path = @manifest.resolver.resolve(ctx.target).path
|
|
188
|
-
return { pass: true } unless File.exist?(path)
|
|
189
|
-
|
|
190
|
-
{ pass: false, error: Textus::Error.new(
|
|
191
|
-
:raw_write_once,
|
|
192
|
-
"raw entry '#{ctx.target}' already exists; " \
|
|
193
|
-
"delete it first (`textus key-delete #{ctx.target}`), then re-ingest",
|
|
194
|
-
) }
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Deletion authority: the lane's write capability OR the author capability.
|
|
198
|
-
# On raw-kind lanes only the author capability grants deletion (correction
|
|
199
|
-
# escape hatch); the lane's own verb (ingest) is write-only. On all other
|
|
200
|
-
# lane kinds the behaviour matches lane_writable_by — the lane's writer
|
|
201
|
-
# can delete as before.
|
|
202
|
-
def evaluate_lane_deletable_by(ctx)
|
|
203
|
-
is_raw = @manifest.policy.declared_kind(ctx.mentry.lane.to_s) == :raw
|
|
204
|
-
pass = if is_raw
|
|
205
|
-
ctx.actor_caps.include?("author")
|
|
206
|
-
else
|
|
207
|
-
ctx.actor_caps.include?(ctx.lane_verb.to_s) || ctx.actor_caps.include?("author")
|
|
208
|
-
end
|
|
209
|
-
return { pass: true } if pass
|
|
210
|
-
|
|
211
|
-
extra_holders = is_raw ? ["author"] : [ctx.lane_verb.to_s, "author"]
|
|
212
|
-
holders = extra_holders.flat_map { |v| @manifest.policy.roles_with_capability(v) }.uniq
|
|
213
|
-
{ pass: false, error: Textus::WriteForbidden.new(ctx.mentry.key, ctx.mentry.lane,
|
|
214
|
-
verb: ctx.lane_verb, holders:) }
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
def schema_reason(err)
|
|
218
|
-
d = err.details
|
|
219
|
-
return err.message.dup unless d.is_a?(Hash)
|
|
220
|
-
return "missing required fields: #{Array(d["missing"]).join(", ")}" if d["missing"]
|
|
221
|
-
return "field '#{d["field"]}': #{d["reason"]}" if d["field"]
|
|
222
|
-
|
|
223
|
-
err.message.dup
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
end
|
data/lib/textus/gate.rb
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
class Gate
|
|
5
|
-
VERB_COMMAND = {
|
|
6
|
-
get: Textus::Command::Get,
|
|
7
|
-
put: Textus::Command::Put,
|
|
8
|
-
propose: Textus::Command::Propose,
|
|
9
|
-
key_delete: Textus::Command::KeyDelete,
|
|
10
|
-
key_mv: Textus::Command::KeyMv,
|
|
11
|
-
accept: Textus::Command::Accept,
|
|
12
|
-
reject: Textus::Command::Reject,
|
|
13
|
-
enqueue: Textus::Command::Enqueue,
|
|
14
|
-
list: Textus::Command::List,
|
|
15
|
-
where: Textus::Command::Where,
|
|
16
|
-
uid: Textus::Command::Uid,
|
|
17
|
-
blame: Textus::Command::Blame,
|
|
18
|
-
audit: Textus::Command::Audit,
|
|
19
|
-
deps: Textus::Command::Deps,
|
|
20
|
-
rdeps: Textus::Command::Rdeps,
|
|
21
|
-
pulse: Textus::Command::Pulse,
|
|
22
|
-
rule_explain: Textus::Command::RuleExplain,
|
|
23
|
-
rule_list: Textus::Command::RuleList,
|
|
24
|
-
rule_lint: Textus::Command::RuleLint,
|
|
25
|
-
published: Textus::Command::Published,
|
|
26
|
-
schema_show: Textus::Command::SchemaShow,
|
|
27
|
-
doctor: Textus::Command::Doctor,
|
|
28
|
-
boot: Textus::Command::Boot,
|
|
29
|
-
jobs: Textus::Command::Jobs,
|
|
30
|
-
data_mv: Textus::Command::DataMv,
|
|
31
|
-
key_mv_prefix: Textus::Command::KeyMvPrefix,
|
|
32
|
-
key_delete_prefix: Textus::Command::KeyDeletePrefix,
|
|
33
|
-
drain: Textus::Command::Drain,
|
|
34
|
-
ingest: Textus::Command::Ingest,
|
|
35
|
-
}.freeze
|
|
36
|
-
|
|
37
|
-
ROUTES = {
|
|
38
|
-
Command::Get => [Textus::Action::Get],
|
|
39
|
-
Command::Put => [Textus::Action::Put],
|
|
40
|
-
Command::Propose => [Textus::Action::Propose],
|
|
41
|
-
Command::KeyDelete => [Textus::Action::KeyDelete],
|
|
42
|
-
Command::KeyMv => [Textus::Action::KeyMv],
|
|
43
|
-
Command::Accept => [Textus::Action::Accept],
|
|
44
|
-
Command::Reject => [Textus::Action::Reject],
|
|
45
|
-
Command::Enqueue => [Textus::Action::Enqueue],
|
|
46
|
-
Command::List => [Textus::Action::List],
|
|
47
|
-
Command::Where => [Textus::Action::Where],
|
|
48
|
-
Command::Uid => [Textus::Action::Uid],
|
|
49
|
-
Command::Blame => [Textus::Action::Blame],
|
|
50
|
-
Command::Audit => [Textus::Action::Audit],
|
|
51
|
-
Command::Deps => [Textus::Action::Deps],
|
|
52
|
-
Command::Rdeps => [Textus::Action::Rdeps],
|
|
53
|
-
Command::Pulse => [Textus::Action::Pulse],
|
|
54
|
-
Command::RuleExplain => [Textus::Action::RuleExplain],
|
|
55
|
-
Command::RuleList => [Textus::Action::RuleList],
|
|
56
|
-
Command::RuleLint => [Textus::Action::RuleLint],
|
|
57
|
-
Command::Published => [Textus::Action::Published],
|
|
58
|
-
Command::SchemaShow => [Textus::Action::SchemaEnvelope],
|
|
59
|
-
Command::Doctor => [Textus::Action::Doctor],
|
|
60
|
-
Command::Boot => [Textus::Action::Boot],
|
|
61
|
-
Command::Jobs => [Textus::Action::Jobs],
|
|
62
|
-
Command::DataMv => [Textus::Action::DataMv],
|
|
63
|
-
Command::KeyMvPrefix => [Textus::Action::KeyMvPrefix],
|
|
64
|
-
Command::KeyDeletePrefix => [Textus::Action::KeyDeletePrefix],
|
|
65
|
-
Command::Drain => [Textus::Action::Drain],
|
|
66
|
-
Command::Ingest => [Textus::Action::Ingest],
|
|
67
|
-
}.freeze
|
|
68
|
-
|
|
69
|
-
def initialize(container)
|
|
70
|
-
@container = container
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def dispatch(cmd, correlation_id: nil)
|
|
74
|
-
cmd = normalize_propose_key(cmd, @container) if cmd.is_a?(Command::Propose)
|
|
75
|
-
action_classes = ROUTES.fetch(cmd.class) do
|
|
76
|
-
raise Textus::UsageError.new("unknown command: #{cmd.class}")
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
Gate::Auth.new(@container).check!(cmd)
|
|
80
|
-
call_obj = build_call(cmd, correlation_id: correlation_id)
|
|
81
|
-
results = action_classes.map { |klass| run_action(klass, cmd, @container, call_obj) }
|
|
82
|
-
results.length == 1 ? results.first : results
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
def normalize_propose_key(cmd, container)
|
|
88
|
-
return cmd if cmd.pending_key
|
|
89
|
-
|
|
90
|
-
zone = container.manifest.policy.propose_lane_for(cmd.role.to_s)
|
|
91
|
-
cmd.with(pending_key: zone ? "#{zone}.#{cmd.key}" : nil)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def run_action(klass, cmd, container, call_obj)
|
|
95
|
-
action = klass.new(**extract_kwargs(klass, cmd))
|
|
96
|
-
action.call(container:, call: call_obj)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def extract_kwargs(klass, cmd)
|
|
100
|
-
params = klass.instance_method(:initialize).parameters
|
|
101
|
-
accepts_keyrest = params.any? { |t, _| t == :keyrest }
|
|
102
|
-
param_set = params.to_set { |_t, n| n }
|
|
103
|
-
cmd.members.each_with_object({}) do |m, h|
|
|
104
|
-
next unless accepts_keyrest || param_set.include?(m)
|
|
105
|
-
|
|
106
|
-
val = cmd.public_send(m)
|
|
107
|
-
h[m] = val unless val.nil?
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def build_call(cmd, correlation_id: nil)
|
|
112
|
-
dry_run = cmd.respond_to?(:dry_run) ? !cmd.dry_run.nil? : false
|
|
113
|
-
Textus::Call.build(role: cmd.role, dry_run:, correlation_id: correlation_id)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
data/lib/textus/jobs/base.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
3
|
-
class Base
|
|
4
|
-
def self.inherited(subclass)
|
|
5
|
-
super
|
|
6
|
-
return unless subclass.name
|
|
7
|
-
|
|
8
|
-
TracePoint.new(:end) do |tp|
|
|
9
|
-
if tp.self == subclass
|
|
10
|
-
Textus::Jobs.register(subclass)
|
|
11
|
-
tp.disable
|
|
12
|
-
end
|
|
13
|
-
end.enable
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(**)
|
|
17
|
-
raise NotImplementedError.new("#{self.class}#call")
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def args = {}
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Jobs
|
|
5
|
-
class Materialize < Base
|
|
6
|
-
TYPE = "materialize"
|
|
7
|
-
|
|
8
|
-
def initialize(key:)
|
|
9
|
-
super()
|
|
10
|
-
@key = key
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def args = { key: @key }
|
|
14
|
-
|
|
15
|
-
def call(container:, call:)
|
|
16
|
-
Textus::Produce::Engine.converge(container: container, call: call, keys: [@key])
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
data/lib/textus/jobs/plan.rb
DELETED
data/lib/textus/jobs/planner.rb
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Jobs
|
|
5
|
-
class Planner
|
|
6
|
-
ACTIONS_BY_TRIGGER = {
|
|
7
|
-
"convergence" => %w[materialize sweep],
|
|
8
|
-
"entry.written" => %w[materialize],
|
|
9
|
-
"entry.deleted" => %w[materialize],
|
|
10
|
-
"entry.moved" => %w[materialize],
|
|
11
|
-
"proposal.accepted" => %w[materialize],
|
|
12
|
-
"proposal.rejected" => %w[materialize],
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
SCOPE_RESOLVERS = {
|
|
16
|
-
"materialize" => :producible_keys,
|
|
17
|
-
"sweep" => :lane_keys,
|
|
18
|
-
}.freeze
|
|
19
|
-
|
|
20
|
-
def self.seed(container:, queue:, role:)
|
|
21
|
-
jobs = new(container: container).plan(
|
|
22
|
-
trigger: { "type" => "convergence" },
|
|
23
|
-
role: role,
|
|
24
|
-
)
|
|
25
|
-
jobs.each { |j| queue.enqueue(j) }
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def initialize(container:)
|
|
29
|
-
@container = container
|
|
30
|
-
@manifest = container.manifest
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def plan(trigger:, role:)
|
|
34
|
-
type = trigger["type"] || trigger[:type]
|
|
35
|
-
trigger["target"] || trigger[:target]
|
|
36
|
-
return [] if type.nil?
|
|
37
|
-
|
|
38
|
-
blocks_with_react = @manifest.rules.blocks.select(&:react)
|
|
39
|
-
if blocks_with_react.any?
|
|
40
|
-
plan_from_rules(blocks_with_react, type, role)
|
|
41
|
-
else
|
|
42
|
-
plan_from_defaults(type, role)
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def plan_from_rules(blocks, type, role)
|
|
49
|
-
jobs = []
|
|
50
|
-
blocks
|
|
51
|
-
.select { |b| matches_trigger?(b.react, type) }
|
|
52
|
-
.each do |block|
|
|
53
|
-
do_action = block.react.raw["do"]
|
|
54
|
-
Array(do_action).each do |action|
|
|
55
|
-
if action == "sweep"
|
|
56
|
-
jobs << Textus::Ports::JobStore::Job.new(
|
|
57
|
-
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
58
|
-
)
|
|
59
|
-
else
|
|
60
|
-
resolver = SCOPE_RESOLVERS.fetch(action, :producible_keys)
|
|
61
|
-
keys = send(resolver, nil)
|
|
62
|
-
keys.each { |key| jobs << job(action, key, role) }
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
jobs
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def plan_from_defaults(type, role)
|
|
70
|
-
actions = ACTIONS_BY_TRIGGER.fetch(type, [])
|
|
71
|
-
jobs = []
|
|
72
|
-
producible_keys(nil).each { |k| jobs << job("materialize", k, role) } if actions.include?("materialize")
|
|
73
|
-
if actions.include?("sweep")
|
|
74
|
-
jobs << Textus::Ports::JobStore::Job.new(
|
|
75
|
-
type: "sweep", args: { "scope" => {} }, enqueued_by: role,
|
|
76
|
-
)
|
|
77
|
-
end
|
|
78
|
-
jobs
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def matches_trigger?(react, type)
|
|
82
|
-
on = react.raw["on"]
|
|
83
|
-
Array(on).include?(type)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def job(type, key, enqueued_by)
|
|
87
|
-
Textus::Ports::JobStore::Job.new(type: type, args: { "key" => key }, enqueued_by: enqueued_by)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def producible_keys(_target)
|
|
91
|
-
@manifest.data.entries
|
|
92
|
-
.select { |e| !e.publish_tree.nil? || !e.publish_to.empty? }
|
|
93
|
-
.map(&:key)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def lane_keys(_target)
|
|
97
|
-
@manifest.data.entries.map(&:key)
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
require "fileutils"
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Jobs
|
|
5
|
-
class Retention
|
|
6
|
-
def initialize(container:, call:)
|
|
7
|
-
@container = container
|
|
8
|
-
@call = call
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def call(rows)
|
|
12
|
-
out = { dropped: [], archived: [], failed: [] }
|
|
13
|
-
rows.each do |row|
|
|
14
|
-
key = row["key"]
|
|
15
|
-
begin
|
|
16
|
-
case row["action"]
|
|
17
|
-
when "drop"
|
|
18
|
-
delete(key)
|
|
19
|
-
out[:dropped] << key
|
|
20
|
-
when "archive"
|
|
21
|
-
archive_leaf(row)
|
|
22
|
-
delete(key)
|
|
23
|
-
out[:archived] << key
|
|
24
|
-
end
|
|
25
|
-
rescue Textus::Error => e
|
|
26
|
-
out[:failed] << { "key" => key, "error" => e.message }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
out
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
private
|
|
33
|
-
|
|
34
|
-
def archive_leaf(row)
|
|
35
|
-
src = row["path"]
|
|
36
|
-
root = @container.root.to_s
|
|
37
|
-
rel = src.delete_prefix("#{root}/")
|
|
38
|
-
dest = File.join(root, "archive", rel)
|
|
39
|
-
FileUtils.mkdir_p(File.dirname(dest))
|
|
40
|
-
FileUtils.cp(src, dest)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def delete(key)
|
|
44
|
-
Textus::Action::KeyDelete.new(key: key).call(container: @container, call: @call)
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
data/lib/textus/jobs/sweep.rb
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
3
|
-
class Sweep < Base
|
|
4
|
-
REQUIRED_ROLE = Textus::Role::AUTOMATION
|
|
5
|
-
TYPE = "sweep"
|
|
6
|
-
|
|
7
|
-
def initialize(scope: nil, key: nil)
|
|
8
|
-
super()
|
|
9
|
-
@scope = scope || {}
|
|
10
|
-
@key = key
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def args = { scope: @scope, key: @key }.compact
|
|
14
|
-
|
|
15
|
-
def call(container:, call:)
|
|
16
|
-
prefix = @key || (@scope.is_a?(Hash) ? @scope["prefix"] : nil)
|
|
17
|
-
lane = @scope.is_a?(Hash) ? @scope["lane"] : nil
|
|
18
|
-
rows = Textus::Core::Retention::Sweep.new(
|
|
19
|
-
manifest: container.manifest,
|
|
20
|
-
file_stat: Textus::Ports::Storage::FileStat.new,
|
|
21
|
-
clock: Textus::Ports::Clock.new,
|
|
22
|
-
).call(prefix: prefix, lane: lane)
|
|
23
|
-
Textus::Jobs::Retention.new(container: container, call: call).call(rows)
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
data/lib/textus/jobs/worker.rb
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Jobs
|
|
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::Jobs.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
|