// transforms.zig - global bookkeeping transforms // // universal transforms that run on every state transition: // - SetStartTime: set start_time when first entering RUNNING // - SetEndTime: set end_time when entering terminal state // - IncrementRunTime: accumulate total_run_time when exiting RUNNING // - IncrementRunCount: increment run_count when entering RUNNING const std = @import("std"); const log = @import("../logging.zig"); const time_util = @import("../utilities/time.zig"); const types = @import("types.zig"); const StateType = types.StateType; /// context for a state transition, holding info needed for bookkeeping pub const TransitionContext = struct { // current run state (from db) current_state_type: ?StateType, current_state_timestamp: ?[]const u8, start_time: ?[]const u8, end_time: ?[]const u8, run_count: i64, total_run_time: f64, // proposed new state proposed_state_type: StateType, proposed_state_timestamp: []const u8, // output: updated values to write to db new_start_time: ?[]const u8 = null, new_end_time: ?[]const u8 = null, new_run_count: i64 = 0, new_total_run_time: f64 = 0.0, }; /// apply all bookkeeping transforms to a state transition pub fn applyBookkeeping(ctx: *TransitionContext) void { // copy current values as baseline ctx.new_start_time = ctx.start_time; ctx.new_end_time = ctx.end_time; ctx.new_run_count = ctx.run_count; ctx.new_total_run_time = ctx.total_run_time; // SetStartTime: record when first entering RUNNING if (ctx.proposed_state_type.isRunning() and ctx.start_time == null) { ctx.new_start_time = ctx.proposed_state_timestamp; log.debug("orchestration", "setting start_time to {s}", .{ctx.proposed_state_timestamp}); } // SetEndTime: record when entering terminal state if (ctx.proposed_state_type.isFinal()) { if (ctx.start_time != null and ctx.end_time == null) { ctx.new_end_time = ctx.proposed_state_timestamp; log.debug("orchestration", "setting end_time to {s}", .{ctx.proposed_state_timestamp}); } } // clear end_time if exiting final state for non-final state if (ctx.current_state_type) |current| { if (current.isFinal() and !ctx.proposed_state_type.isFinal()) { ctx.new_end_time = null; log.debug("orchestration", "clearing end_time (exiting terminal state)", .{}); } } // IncrementRunTime: accumulate time spent in RUNNING if (ctx.current_state_type) |current| { if (current.isRunning()) { if (ctx.current_state_timestamp) |start_ts| { const duration = computeDuration(start_ts, ctx.proposed_state_timestamp); ctx.new_total_run_time = ctx.total_run_time + duration; log.debug("orchestration", "adding {d:.3}s to total_run_time (now {d:.3}s)", .{ duration, ctx.new_total_run_time }); } } } // IncrementRunCount: bump count when entering RUNNING if (ctx.proposed_state_type.isRunning()) { ctx.new_run_count = ctx.run_count + 1; log.debug("orchestration", "incrementing run_count to {d}", .{ctx.new_run_count}); } } /// compute duration in seconds between two ISO8601 timestamps fn computeDuration(start: []const u8, end: []const u8) f64 { const start_us = time_util.parse(start) orelse return 0.0; const end_us = time_util.parse(end) orelse return 0.0; if (end_us >= start_us) { return @as(f64, @floatFromInt(end_us - start_us)) / 1_000_000.0; } return 0.0; } // ============================================================================ // Tests // ============================================================================ test "computeDuration" { const testing = std.testing; const duration = computeDuration("2024-01-19T16:30:00Z", "2024-01-19T16:30:05Z"); try testing.expectApproxEqAbs(@as(f64, 5.0), duration, 0.001); } test "computeDuration with fractional seconds" { const testing = std.testing; const duration = computeDuration("2024-01-19T16:30:00.000000Z", "2024-01-19T16:30:01.500000Z"); try testing.expectApproxEqAbs(@as(f64, 1.5), duration, 0.001); }