Files
rvbox/internal/server/store/incidents_read.go
T

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
}