// rules.zig - orchestration rule abstraction // // defines the rule interface and policy application logic const std = @import("std"); const log = @import("../logging.zig"); const types = @import("types.zig"); pub const StateType = types.StateType; pub const StateTypeSet = types.StateTypeSet; pub const ResponseStatus = types.ResponseStatus; pub const ResponseDetails = types.ResponseDetails; pub const OrchestrationResult = types.OrchestrationResult; /// context passed to orchestration rules during state transition pub const RuleContext = struct { // transition info initial_state: ?StateType, proposed_state: StateType, initial_state_timestamp: ?[]const u8, proposed_state_timestamp: []const u8, // scheduling info (for CopyScheduledTime, WaitForScheduledTime) initial_scheduled_time: ?[]const u8 = null, // next_scheduled_start_time from SCHEDULED state proposed_scheduled_time: ?[]const u8 = null, // scheduled_time from proposed state (if any) // idempotency (for PreventDuplicateTransitions) initial_transition_id: ?[]const u8 = null, // transition_id from current state proposed_transition_id: ?[]const u8 = null, // transition_id from proposed state // run metadata (for rules that need it) run_id: []const u8, flow_id: ?[]const u8 = null, deployment_id: ?[]const u8 = null, // retry policy (from empirical_policy) run_count: i64 = 0, retries: ?i64 = null, // max retries allowed retry_delay: ?i64 = null, // delay in seconds between retries // orchestration result (modified by rules) result: OrchestrationResult = .{}, // output: values to write back to db (set by rules) new_expected_start_time: ?[]const u8 = null, // retry output: when RetryFailedFlows rejects, it proposes a new state retry_state_type: ?StateType = null, retry_state_name: ?[]const u8 = null, retry_scheduled_time: ?[]const u8 = null, // internal buffer for retry_scheduled_time (to avoid use-after-free from stack buffers) _retry_time_buf: [32]u8 = undefined, // empirical_policy mutation flags (set by RetryFailedFlows) // when set, API handler should update the policy JSON accordingly set_retry_type_in_process: bool = false, // set retry_type = "in_process" clear_retry_type: bool = false, // set retry_type = null (when retries exhausted) clear_pause_keys: bool = false, // set pause_keys = [] set_resuming_false: bool = false, // set resuming = false /// reject the transition with a reason pub fn reject(self: *RuleContext, reason: []const u8) void { self.result.status = .REJECT; self.result.details.reason = reason; log.debug("orchestration", "rule rejected transition: {s}", .{reason}); } /// delay the transition (client should retry) pub fn wait(self: *RuleContext, reason: []const u8, retry_after: f64) void { self.result.status = .WAIT; self.result.details.reason = reason; self.result.details.retry_after = retry_after; log.debug("orchestration", "rule delayed transition: {s} (retry after {d}s)", .{ reason, retry_after }); } /// abort the transition completely pub fn abort(self: *RuleContext, reason: []const u8) void { self.result.status = .ABORT; self.result.details.reason = reason; log.debug("orchestration", "rule aborted transition: {s}", .{reason}); } /// reject and schedule a retry (used by RetryFailedFlows) /// The caller should set retry_scheduled_time before calling this pub fn scheduleRetry(self: *RuleContext, reason: []const u8, scheduled_time: []const u8) void { self.result.status = .REJECT; self.result.details.reason = reason; self.retry_state_type = .SCHEDULED; self.retry_state_name = "AwaitingRetry"; self.retry_scheduled_time = scheduled_time; // set policy mutation flags for retry self.set_retry_type_in_process = true; self.set_resuming_false = true; self.clear_pause_keys = true; log.debug("orchestration", "rule scheduled retry: {s} at {s}", .{ reason, scheduled_time }); } /// signal that retries are exhausted and retry_type should be cleared pub fn clearRetryType(self: *RuleContext) void { self.clear_retry_type = true; } /// check if transition is still accepted (not yet rejected/waited/aborted) pub fn isAccepted(self: *const RuleContext) bool { return self.result.status == .ACCEPT; } }; /// an orchestration rule that can modify or reject state transitions pub const OrchestrationRule = struct { /// rule name for logging/debugging name: []const u8, /// which initial states this rule applies to from_states: StateTypeSet, /// which proposed states this rule applies to to_states: StateTypeSet, /// the rule implementation - called before state is committed before_transition: *const fn (*RuleContext) void, /// check if this rule applies to the given transition pub fn appliesTo(self: OrchestrationRule, initial: ?StateType, proposed: StateType) bool { const from_matches = if (initial) |s| self.from_states.contains(s) else self.from_states.containsNull(); const to_matches = self.to_states.contains(proposed); return from_matches and to_matches; } }; /// apply all applicable rules from a policy to a transition pub fn applyPolicy( policy: []const OrchestrationRule, ctx: *RuleContext, ) void { for (policy) |rule| { // skip rules that don't apply to this transition if (!rule.appliesTo(ctx.initial_state, ctx.proposed_state)) { continue; } log.debug("orchestration", "applying rule: {s}", .{rule.name}); // apply the rule rule.before_transition(ctx); // if rule rejected/waited/aborted, stop processing if (!ctx.isAccepted()) { log.debug("orchestration", "rule {s} stopped transition with status {s}", .{ rule.name, ctx.result.status.toString(), }); return; } } }