feat: implement control history and mutation APIs
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IncidentView struct {
|
||||
IncidentUUID [16]byte
|
||||
DetectedAt time.Time
|
||||
ResolvedAt *time.Time
|
||||
State IncidentState
|
||||
Kind IncidentKind
|
||||
Scope IncidentScope
|
||||
ClientID string
|
||||
IssueUUID *[16]byte
|
||||
Summary string
|
||||
DataLoss bool
|
||||
AutomaticallyRepairable bool
|
||||
}
|
||||
|
||||
type IncidentPage struct {
|
||||
IncludeResolved bool
|
||||
Limit uint32
|
||||
SnapshotBoundary int64
|
||||
AfterDetectedAt int64
|
||||
AfterUUID [16]byte
|
||||
HasAfter bool
|
||||
}
|
||||
|
||||
func (store *Store) ListIncidentViews(ctx context.Context, page IncidentPage) ([]IncidentView, bool, error) {
|
||||
if page.Limit == 0 || page.Limit > 1000 || page.SnapshotBoundary <= 0 {
|
||||
return nil, false, errors.New("invalid incident page")
|
||||
}
|
||||
database, err := store.openDatabase()
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
query := `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE detected_at <= ?`
|
||||
args := []any{page.SnapshotBoundary}
|
||||
if !page.IncludeResolved {
|
||||
query += ` AND state = 1`
|
||||
}
|
||||
if page.HasAfter {
|
||||
query += ` AND (detected_at < ? OR (detected_at = ? AND incident_uuid < ?))`
|
||||
args = append(args, page.AfterDetectedAt, page.AfterDetectedAt, page.AfterUUID[:])
|
||||
}
|
||||
query += ` ORDER BY detected_at DESC, incident_uuid DESC LIMIT ?`
|
||||
args = append(args, page.Limit+1)
|
||||
rows, err := database.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
result := make([]IncidentView, 0, page.Limit)
|
||||
for rows.Next() {
|
||||
view, scanErr := scanIncidentView(rows)
|
||||
if scanErr != nil {
|
||||
return nil, false, scanErr
|
||||
}
|
||||
if uint32(len(result)) < page.Limit {
|
||||
result = append(result, view)
|
||||
} else {
|
||||
return result, true, rows.Err()
|
||||
}
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return result, false, nil
|
||||
}
|
||||
|
||||
func (store *Store) GetIncidentView(ctx context.Context, incidentUUID [16]byte) (IncidentView, error) {
|
||||
if incidentUUID == [16]byte{} {
|
||||
return IncidentView{}, ErrIncidentNotFound
|
||||
}
|
||||
database, err := store.openDatabase()
|
||||
if err != nil {
|
||||
return IncidentView{}, err
|
||||
}
|
||||
view, err := scanIncidentView(database.QueryRowContext(ctx, `SELECT incident_uuid, detected_at, resolved_at, state, kind, scope, client_id, issue_uuid, summary, data_loss, automatically_repairable FROM storage_incidents WHERE incident_uuid = ?`, incidentUUID[:]))
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return IncidentView{}, ErrIncidentNotFound
|
||||
}
|
||||
return view, err
|
||||
}
|
||||
|
||||
func scanIncidentView(scanner interface{ Scan(...any) error }) (IncidentView, error) {
|
||||
var view IncidentView
|
||||
var encoded, issue []byte
|
||||
var detected int64
|
||||
var resolved sql.NullInt64
|
||||
var state, kind uint32
|
||||
var client sql.NullString
|
||||
var dataLoss, repairable int
|
||||
if err := scanner.Scan(&encoded, &detected, &resolved, &state, &kind, &view.Scope, &client, &issue, &view.Summary, &dataLoss, &repairable); err != nil {
|
||||
return view, err
|
||||
}
|
||||
if len(encoded) != 16 || (issue != nil && len(issue) != 16) || state < uint32(IncidentOpen) || state > uint32(IncidentAcknowledged) || kind < uint32(IncidentUncommittedTail) || kind > uint32(IncidentDiskExhaustion) {
|
||||
return view, ErrInvalidSegmentRecord
|
||||
}
|
||||
copy(view.IncidentUUID[:], encoded)
|
||||
view.DetectedAt = time.Unix(0, detected).UTC()
|
||||
view.ResolvedAt = nullableTime(resolved)
|
||||
view.State, view.Kind = IncidentState(state), IncidentKind(kind)
|
||||
if client.Valid {
|
||||
view.ClientID = client.String
|
||||
}
|
||||
view.DataLoss, view.AutomaticallyRepairable = dataLoss == 1, repairable == 1
|
||||
if issue != nil {
|
||||
value := [16]byte{}
|
||||
copy(value[:], issue)
|
||||
view.IssueUUID = &value
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
Reference in New Issue
Block a user