diff --git a/internal/oplog/oplog.go b/internal/oplog/oplog.go index d5843df..be356dd 100644 --- a/internal/oplog/oplog.go +++ b/internal/oplog/oplog.go @@ -57,7 +57,10 @@ func NewOpLog(databasePath string) (*OpLog, error) { return nil, fmt.Errorf("error opening database: %s", err) } - o := &OpLog{db: db} + o := &OpLog{ + db: db, + } + o.nextId.Store(1) if err := db.Update(func(tx *bolt.Tx) error { // Create the buckets if they don't exist @@ -194,15 +197,15 @@ func (o *OpLog) getOperationHelper(b *bolt.Bucket, id int64) (*v1.Operation, err func (o *OpLog) addOperationHelper(tx *bolt.Tx, op *v1.Operation) error { b := tx.Bucket(OpLogBucket) - if op.Id == 0 { - // Create a unique ID sorted based on the start time in milliseconds and - // a counter to ensure uniqueness in the case of multiple operations - // starting at the same time. - op.Id = op.UnixTimeStartMs<<20 | (o.nextId.Add(1) & (1<<20 - 1)) - if op.Id < 0 { - return fmt.Errorf("overflow in operation ID generation") + seq, err := b.NextSequence() + if err != nil { + return fmt.Errorf("error getting next sequence: %w", err) } + if op.UnixTimeStartMs == 0 { + return fmt.Errorf("operation must have a start time") + } + op.Id = op.UnixTimeStartMs<<20 | int64(seq&(1<<20-1)) } op.SnapshotId = NormalizeSnapshotId(op.SnapshotId) diff --git a/internal/orchestrator/tasks.go b/internal/orchestrator/tasks.go index 2e291fd..6e2889a 100644 --- a/internal/orchestrator/tasks.go +++ b/internal/orchestrator/tasks.go @@ -47,6 +47,21 @@ func (t *ScheduledBackupTask) Name() string { } func (t *ScheduledBackupTask) Next(now time.Time) *time.Time { + if ops, err := t.orchestrator.OpLog.GetByPlan(t.plan.Id, indexutil.CollectLastN(10)); err == nil { + var lastBackupOp *v1.Operation + for _, op := range ops { + if _, ok := op.Op.(*v1.Operation_OperationBackup); ok { + lastBackupOp = op + } + } + + if lastBackupOp != nil { + now = time.Unix(0, lastBackupOp.UnixTimeEndMs*int64(time.Millisecond)) + } + } else { + zap.S().Errorf("error getting last operation for plan %q when computing backup schedule: %v", t.plan.Id, err) + } + next := t.schedule.Next(now) return &next } @@ -174,6 +189,7 @@ func indexSnapshotsHelper(ctx context.Context, orchestrator *Orchestrator, plan UnixTimeStartMs: snapshotProto.UnixTimeMs, UnixTimeEndMs: snapshotProto.UnixTimeMs, Status: v1.OperationStatus_STATUS_SUCCESS, + SnapshotId: snapshotProto.Id, Op: &v1.Operation_OperationIndexSnapshot{ OperationIndexSnapshot: &v1.OperationIndexSnapshot{ Snapshot: snapshotProto, @@ -196,15 +212,6 @@ func indexSnapshotsHelper(ctx context.Context, orchestrator *Orchestrator, plan return err } -func containsSnapshotOperation(ops []*v1.Operation) bool { - for _, op := range ops { - if _, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { - return true - } - } - return false -} - // WithOperation is a utility that creates an operation to track the function's execution. // timestamps are automatically added and the status is automatically updated if an error occurs. func WithOperation(oplog *oplog.OpLog, op *v1.Operation, do func() error) error { @@ -233,3 +240,12 @@ func curTimeMillis() int64 { t := time.Now() return t.Unix()*1000 + int64(t.Nanosecond()/1000000) } + +func containsSnapshotOperation(ops []*v1.Operation) bool { + for _, op := range ops { + if _, ok := op.Op.(*v1.Operation_OperationIndexSnapshot); ok { + return true + } + } + return false +} diff --git a/proto/v1/operations.proto b/proto/v1/operations.proto index 39c0dcf..6c2cb26 100644 --- a/proto/v1/operations.proto +++ b/proto/v1/operations.proto @@ -12,13 +12,18 @@ message OperationList { message Operation { int64 id = 1; - string repo_id = 2; // repo id if associated with a repo (always true) - string plan_id = 3; // plan id if associated with a plan (always true) - string snapshot_id = 8; // snapshot id if associated with a snapshot. + // repo id if associated with a repo (always true) + string repo_id = 2; + // plan id if associated with a plan (always true) + string plan_id = 3; + // snapshot id if associated with a snapshot. + string snapshot_id = 8; OperationStatus status = 4; - int64 unix_time_start_ms = 5; + // unix time in milliseconds of the operation's creation (ID is derived from this) + int64 unix_time_start_ms = 5; int64 unix_time_end_ms = 6; - string display_message = 7; // human readable context message (if any) + // human readable context message, typically an error message. + string display_message = 7; oneof op { OperationBackup operation_backup = 100; diff --git a/webui/src/components/OperationList.tsx b/webui/src/components/OperationList.tsx index 7da42a9..9dc6728 100644 --- a/webui/src/components/OperationList.tsx +++ b/webui/src/components/OperationList.tsx @@ -100,17 +100,17 @@ export const OperationRow = ({ color = "blue"; } + let opType = "Message"; + if (operation.operationBackup) { + opType = "Backup"; + } else if (operation.operationIndexSnapshot) { + opType = "Snapshot"; + } + if ( operation.displayMessage && operation.status === OperationStatus.STATUS_ERROR ) { - let opType = "Message"; - if (operation.operationBackup) { - opType = "Backup"; - } else if (operation.operationIndexSnapshot) { - opType = "Snapshot"; - } - return (