diff --git a/core/db_interface.go b/core/db_interface.go new file mode 100644 index 0000000..faa753a --- /dev/null +++ b/core/db_interface.go @@ -0,0 +1,94 @@ +package core + +import ( + "time" + + . "git.exozy.me/exozyme/status/scanner" +) + +// change implementation here +func OpenDatabase(dir string) (Database, error) { + return OpenPrototypeDb(dir) +} + +type Database interface { + // dump data to disk + Persist() error + + // get all records after some time (`cutoff`) + // + // Usage: (get past 7 days of history) + // db.QueryAllAfter(time.Now().Add(-7 * 24 * time.Hour)) + // + // Then, parse the result with `GetServiceStatusSpans(rows, service_url)` + QueryAllAfter(cutoff time.Time) ([]Row, error) + + // store service status records with timestamp + // returns non-duplicate rows + StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) + + // store self up/down event with timestamp + // + // `status` is EventSelfUp or EventSelfDown + StoreSelfStatus(status EventType, when time.Time) error +} + +type Row struct { + time time.Time + kind EventType + data ServiceStatus +} + +type EventType int + +const ( + // represents when a servire's status text changes + // start and stop events are represented with EventSelfUp and EventSelfDown + EventService EventType = iota + // represents when this program starts + EventSelfUp + // represents when this program stops + EventSelfDown +) + +type Span struct { + start time.Time + stop time.Time + status ServiceStatus +} + +func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) { + var stop_time time.Time + if stop_i < 0 { + stop_time = time.Now() + } else { + stop_time = rows[stop_i].time + } + start_time := rows[start_i].time + *res = append(*res, Span{start_time, stop_time, status}) +} + +// Find when the service is up, and when the service is down +// returns timespans. There might be gaps between two spans, meaning that we don't know the state of that service +func CalculateServiceStatusSpans(rows []Row, service_url string) []Span { + res := []Span{} + start_i := -1 + var status ServiceStatus + + for i, v := range rows { + switch v.kind { + case EventSelfDown: + append_close_span(&res, rows, start_i, i, status) + start_i = -1 + case EventService: + if v.data.Url == service_url { + append_close_span(&res, rows, start_i, i, status) + start_i = i + status = v.data + } + } + } + append_close_span(&res, rows, start_i, -1, status) + + return res +} diff --git a/core/database.go b/core/db_prototype.go similarity index 60% rename from core/database.go rename to core/db_prototype.go index 41d3f9f..fa029ea 100644 --- a/core/database.go +++ b/core/db_prototype.go @@ -13,99 +13,17 @@ import ( "github.com/oklog/ulid/v2" - . "git.exozy.me/exozyme/status/scanner" + . "git.exozy.me/exozyme/status/scanner" ) -type Row struct { - time time.Time - kind EventType - data ServiceStatus -} - -type EventType int - -const ( - // represents when a servire's status text changes - // start and stop events are represented with EventSelfUp and EventSelfDown - EventService EventType = iota - // represents when this program starts - EventSelfUp - // represents when this program stops - EventSelfDown -) - -type Database interface { - // dump data to disk - Persist() error - - // get all records after some time (`cutoff`) - // - // Usage: (get past 7 days of history) - // db.QueryAllAfter(time.Now().Add(-7 * 24 * time.Hour)) - // - // Then, parse the result with `GetServiceStatusSpans(rows, service_url)` - QueryAllAfter(cutoff time.Time) ([]Row, error) - - // store service status records with timestamp - // returns non-duplicate rows - StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) - - // store self up/down event with timestamp - // - // `status` is EventSelfUp or EventSelfDown - StoreSelfStatus(status EventType, when time.Time) error -} - -type Span struct { - start time.Time - stop time.Time - status ServiceStatus -} - -func append_close_span(res *[]Span, rows []Row, start_i, stop_i int, status ServiceStatus) { - var stop_time time.Time - if stop_i < 0 { - stop_time = time.Now() - } else { - stop_time = rows[stop_i].time - } - start_time := rows[start_i].time - *res = append(*res, Span{start_time, stop_time, status}) -} - -// Find when the service is up, and when the service is down -// returns timespans. There might be gaps between two spans, meaning that we don't know the state of that service -func CalculateServiceStatusSpans(rows []Row, service_url string) []Span { - res := []Span{} - start_i := -1 - var status ServiceStatus - - for i, v := range rows { - switch v.kind { - case EventSelfDown: - append_close_span(&res, rows, start_i, i, status) - start_i = -1 - case EventService: - if v.data.Url == service_url { - append_close_span(&res, rows, start_i, i, status) - start_i = i - status = v.data - } - } - } - append_close_span(&res, rows, start_i, -1, status) - - return res -} - -type dumbDatabase struct { +type PrototypeDb struct { rootDir string rows []Row mutex sync.Mutex } // Load the newest backup, or start anew -func OpenDatabase(dir string) (Database, error) { +func OpenPrototypeDb(dir string) (Database, error) { if dir == "" { return nil, fmt.Errorf("database path is empty. did you set it in config file?") } @@ -141,13 +59,13 @@ func OpenDatabase(dir string) (Database, error) { } log.Printf("Loaded %d rows from: %v", len(rows), fullpath) - return &dumbDatabase{rootDir: dir, rows: rows}, nil + return &PrototypeDb{rootDir: dir, rows: rows}, nil } - return &dumbDatabase{rootDir: dir, rows: []Row{}}, nil + return &PrototypeDb{rootDir: dir, rows: []Row{}}, nil } -func (db *dumbDatabase) sortRows() { +func (db *PrototypeDb) sortRows() { sort.Slice(db.rows, func(i, j int) bool { return db.rows[i].time.Compare(db.rows[j].time) == -1 }) @@ -157,7 +75,7 @@ func (db *dumbDatabase) sortRows() { // // Steps: // create -> write -> fsync -> rename -func (db *dumbDatabase) Persist() error { +func (db *PrototypeDb) Persist() error { db.mutex.Lock() defer db.mutex.Unlock() @@ -215,7 +133,7 @@ func (db *dumbDatabase) Persist() error { return nil } -func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) { +func (db *PrototypeDb) QueryAllAfter(cutoff time.Time) ([]Row, error) { db.mutex.Lock() defer db.mutex.Unlock() @@ -235,7 +153,7 @@ func (db *dumbDatabase) QueryAllAfter(cutoff time.Time) ([]Row, error) { return db.rows[start:], nil } -func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) { +func (db *PrototypeDb) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) { non_duplicates := []ServiceStatus{} db.mutex.Lock() @@ -264,7 +182,7 @@ func (db *dumbDatabase) StoreServiceStatusBatch(entries ServiceStatusBatch, when return non_duplicates, nil } -func (db *dumbDatabase) StoreSelfStatus(status EventType, when time.Time) error { +func (db *PrototypeDb) StoreSelfStatus(status EventType, when time.Time) error { db.mutex.Lock() defer db.mutex.Unlock() diff --git a/core/db_tkrzw.go b/core/db_tkrzw.go new file mode 100644 index 0000000..b7a6cfe --- /dev/null +++ b/core/db_tkrzw.go @@ -0,0 +1,53 @@ +package core + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "os" + "path" + "sort" + "sync" + "time" + + "github.com/oklog/ulid/v2" + "github.com/estraier/tkrzw-go" + + . "git.exozy.me/exozyme/status/scanner" +) + +type Tkrzw struct { + *tkrzw.DBM +} + +func OpenTkrzw(dir string) (Database, error) { + dbm := tkrzw.NewDBM() + status := dbm.Open(path.Join(dir, "casket.tks"), true, map[string]string{}) + if !status.IsOK() { + return nil, status + } + return &Tkrzw{DBM: dbm}, nil + // dbm.Rebuild() +} + +func (dbm *Tkrzw) Persist() error { + status := dbm.Synchronize(true, map[string]string{}) + if !status.IsOK() { + return status + } + return nil +} + +func (dbm *Tkrzw) QueryAllAfter(cutoff time.Time) ([]Row, error) { + panic("not implemented") // TODO: Implement +} + +func (dbm *Tkrzw) StoreServiceStatusBatch(entries ServiceStatusBatch, when time.Time) ([]ServiceStatus, error) { + panic("not implemented") // TODO: Implement +} + +func (dbm *Tkrzw) StoreSelfStatus(status EventType, when time.Time) error { + panic("not implemented") // TODO: Implement +} + diff --git a/go.mod b/go.mod index 4ea7200..65bf373 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21.1 require ( github.com/cbroglie/mustache v1.4.0 + github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab github.com/oklog/ulid/v2 v2.1.0 github.com/pelletier/go-toml/v2 v2.1.0 github.com/prometheus-community/pro-bing v0.3.0 @@ -11,7 +12,6 @@ require ( ) require ( - github.com/elnormous/contenttype v1.0.4 github.com/google/uuid v1.3.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect diff --git a/go.sum b/go.sum index 3947672..8de935f 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elnormous/contenttype v1.0.4 h1:FjmVNkvQOGqSX70yvocph7keC8DtmJaLzTTq6ZOQCI8= -github.com/elnormous/contenttype v1.0.4/go.mod h1:5KTOW8m1kdX1dLMiUJeN9szzR2xkngiv2K+RVZwWBbI= +github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab h1:yQbaZGVW+i4XGqOCmngDuuusApVBBqIDPaLMI6JOyXU= +github.com/estraier/tkrzw-go v0.0.0-20240603165139-89707b45d5ab/go.mod h1:vUOsQ39pa/SuhSmt/Nj+FiNFEHhGf58xM61e0EfZQcc= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=