119 lines
3.8 KiB
Go
119 lines
3.8 KiB
Go
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
|
|
}
|