From d5981249b1e8fc85661d7427cfa96ab0c10eef8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Thu, 26 Dec 2024 15:26:33 +0100
Subject: [PATCH] Add time information
---
acid.adoc | 11 +++++
acid.go | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 130 insertions(+), 8 deletions(-)
diff --git a/acid.adoc b/acid.adoc
index 10c3c57..b60cc80 100644
--- a/acid.adoc
+++ b/acid.adoc
@@ -67,6 +67,17 @@ which has the following fields:
*RunnerName*::
Descriptive name of the runner.
+// Intentionally not documenting CreatedUnix, ChangedUnix, DurationSeconds,
+// which can be derived from the objects.
+*Created*, *Changed*::
+ `*time.Time` of task creation and last task state change respectively,
+ or nil if not known.
+*CreatedAgo*, *ChangedAgo*::
+ Abbreviated human-friendly relative elapsed time duration
+ since *Created* and *Changed* respectively.
+*Duration*::
+ `*time.Duration` of the last run in seconds, or nil if not known.
+
*URL*::
*acid* link to the task, where its log output can be seen.
*RepoURL*::
diff --git a/acid.go b/acid.go
index 2a74b84..00d5a4e 100644
--- a/acid.go
+++ b/acid.go
@@ -30,6 +30,7 @@ import (
"syscall"
ttemplate "text/template"
"time"
+ "unicode"
_ "github.com/mattn/go-sqlite3"
"github.com/pkg/sftp"
@@ -173,6 +174,7 @@ func giteaNewRequest(ctx context.Context, method, path string, body io.Reader) (
func getTasks(ctx context.Context, query string, args ...any) ([]Task, error) {
rows, err := gDB.QueryContext(ctx, `
SELECT id, owner, repo, hash, runner,
+ created, changed, duration,
state, detail, notified,
runlog, tasklog, deploylog FROM task `+query, args...)
if err != nil {
@@ -184,11 +186,13 @@ func getTasks(ctx context.Context, query string, args ...any) ([]Task, error) {
for rows.Next() {
var t Task
err := rows.Scan(&t.ID, &t.Owner, &t.Repo, &t.Hash, &t.Runner,
+ &t.CreatedUnix, &t.ChangedUnix, &t.DurationSeconds,
&t.State, &t.Detail, &t.Notified,
&t.RunLog, &t.TaskLog, &t.DeployLog)
if err != nil {
return nil, err
}
+ // We could also update some fields from gRunning.
tasks = append(tasks, t)
}
return tasks, rows.Err()
@@ -209,6 +213,8 @@ var templateTasks = template.Must(template.New("tasks").Parse(`
ID
+ Created
+ Changed
Repository
Hash
Runner
@@ -221,6 +227,8 @@ var templateTasks = template.Must(template.New("tasks").Parse(`
{{range .}}
{{.ID}}
+ {{.CreatedAgo}}
+ {{.ChangedAgo}}
{{.FullName}}
{{.Hash}}
{{.RunnerName}}
@@ -262,6 +270,14 @@ var templateTask = template.Must(template.New("tasks").Parse(`
Tasks » {{.ID}}
+{{if .Created}}
+
{{if .RunLog}}
Runner log
@@ -324,6 +344,8 @@ func handleTask(w http.ResponseWriter, r *http.Request) {
return
}
+ task.DurationSeconds = rt.elapsed()
+
rt.RunLog.Lock()
defer rt.RunLog.Unlock()
rt.TaskLog.Lock()
@@ -364,8 +386,9 @@ func createTasks(ctx context.Context,
}
defer tx.Rollback()
- stmt, err := tx.Prepare(`INSERT INTO task(owner, repo, hash, runner)
- VALUES (?, ?, ?, ?)`)
+ stmt, err := tx.Prepare(
+ `INSERT INTO task(owner, repo, hash, runner, created, changed)
+ VALUES (?, ?, ?, ?, unixepoch('now'), unixepoch('now'))`)
if err != nil {
return err
}
@@ -446,8 +469,11 @@ func rpcRestartOne(ctx context.Context, id int64) error {
// The executor bumps to "running" after inserting into gRunning,
// so we should not need to exclude that state here.
- result, err := gDB.ExecContext(ctx, `UPDATE task
- SET state = ?, detail = '', notified = 0 WHERE id = ?`,
+ //
+ // We deliberately do not clear previous run data (duration, *log).
+ result, err := gDB.ExecContext(ctx,
+ `UPDATE task SET changed = unixepoch('now'),
+ state = ?, detail = '', notified = 0 WHERE id = ?`,
taskStateNew, id)
if err != nil {
return fmt.Errorf("%d: %w", id, err)
@@ -840,6 +866,9 @@ func newRunningTask(task Task) (*RunningTask, error) {
rt := &RunningTask{DB: task}
config := getConfig()
+ // This is for our own tracking, not actually written to database.
+ rt.DB.ChangedUnix = time.Now().Unix()
+
var ok bool
rt.Runner, ok = config.Runners[rt.DB.Runner]
if !ok {
@@ -935,6 +964,10 @@ func (rt *RunningTask) localEnv() []string {
)
}
+func (rt *RunningTask) elapsed() int64 {
+ return int64(time.Since(time.Unix(rt.DB.ChangedUnix, 0)).Seconds())
+}
+
// update stores the running task's state in the database.
func (rt *RunningTask) update() error {
for _, i := range []struct {
@@ -951,6 +984,7 @@ func (rt *RunningTask) update() error {
*i.log = []byte{}
}
}
+ rt.DB.DurationSeconds = rt.elapsed()
return rt.DB.update()
}
@@ -1175,6 +1209,7 @@ func executorConnect(
func executorRunTask(ctx context.Context, task Task) error {
rt, err := newRunningTask(task)
if err != nil {
+ task.DurationSeconds = 0
task.State, task.Detail = taskStateError, "Misconfigured"
task.Notified = 0
task.RunLog = []byte(err.Error())
@@ -1194,6 +1229,7 @@ func executorRunTask(ctx context.Context, task Task) error {
f()
}
locked(func() {
+ rt.DB.DurationSeconds = 0
rt.DB.State, rt.DB.Detail = taskStateRunning, ""
rt.DB.Notified = 0
rt.DB.RunLog = []byte{}
@@ -1402,6 +1438,11 @@ type Task struct {
Hash string
Runner string
+ // True database names for these are occupied by accessors.
+ CreatedUnix int64
+ ChangedUnix int64
+ DurationSeconds int64
+
State taskState
Detail string
Notified int64
@@ -1437,10 +1478,64 @@ func (t *Task) CloneURL() string {
return fmt.Sprintf("%s/%s/%s.git", getConfig().Gitea, t.Owner, t.Repo)
}
+func shortDurationString(d time.Duration) string {
+ rs := []rune(d.Truncate(time.Second).String())
+ for i, r := range rs {
+ if !unicode.IsLetter(r) {
+ continue
+ }
+ i++
+ for i < len(rs) && unicode.IsLetter(rs[i]) {
+ i++
+ }
+ return string(rs[:i])
+ }
+ return string(rs)
+}
+
+func (t *Task) Created() *time.Time {
+ if t.CreatedUnix == 0 {
+ return nil
+ }
+ tt := time.Unix(t.CreatedUnix, 0)
+ return &tt
+}
+func (t *Task) Changed() *time.Time {
+ if t.ChangedUnix == 0 {
+ return nil
+ }
+ tt := time.Unix(t.ChangedUnix, 0)
+ return &tt
+}
+
+func (t *Task) CreatedAgo() string {
+ if t.CreatedUnix == 0 {
+ return ""
+ }
+ return shortDurationString(time.Since(*t.Created()))
+}
+
+func (t *Task) ChangedAgo() string {
+ if t.ChangedUnix == 0 {
+ return ""
+ }
+ return shortDurationString(time.Since(*t.Changed()))
+}
+
+func (t *Task) Duration() *time.Duration {
+ if t.DurationSeconds == 0 {
+ return nil
+ }
+ td := time.Duration(t.DurationSeconds * int64(time.Second))
+ return &td
+}
+
func (t *Task) update() error {
- _, err := gDB.ExecContext(context.Background(), `UPDATE task
- SET state = ?, detail = ?, notified = ?,
+ _, err := gDB.ExecContext(context.Background(),
+ `UPDATE task SET changed = unixepoch('now'), duration = ?,
+ state = ?, detail = ?, notified = ?,
runlog = ?, tasklog = ?, deploylog = ? WHERE id = ?`,
+ t.DurationSeconds,
t.State, t.Detail, t.Notified,
t.RunLog, t.TaskLog, t.DeployLog, t.ID)
if err == nil {
@@ -1462,6 +1557,10 @@ CREATE TABLE IF NOT EXISTS task(
hash TEXT NOT NULL, -- commit hash
runner TEXT NOT NULL, -- the runner to use
+ created INTEGER NOT NULL DEFAULT 0, -- creation timestamp
+ changed INTEGER NOT NULL DEFAULT 0, -- last state change timestamp
+ duration INTEGER NOT NULL DEFAULT 0, -- duration of last run
+
state INTEGER NOT NULL DEFAULT 0, -- task state
detail TEXT NOT NULL DEFAULT '', -- task state detail
notified INTEGER NOT NULL DEFAULT 0, -- Gitea knows the state
@@ -1519,13 +1618,25 @@ func dbOpen(path string) error {
`task`, `deploylog`, `BLOB NOT NULL DEFAULT x''`); err != nil {
return err
}
- break
case 1:
+ if err = dbEnsureColumn(tx,
+ `task`, `created`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
+ return err
+ }
+ if err = dbEnsureColumn(tx,
+ `task`, `changed`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
+ return err
+ }
+ if err = dbEnsureColumn(tx,
+ `task`, `duration`, `INTEGER NOT NULL DEFAULT 0`); err != nil {
+ return err
+ }
+ case 2:
// The next migration goes here, remember to increment the number below.
}
if _, err = tx.Exec(
- `PRAGMA user_version = ` + strconv.Itoa(1)); err != nil {
+ `PRAGMA user_version = ` + strconv.Itoa(2)); err != nil {
return err
}
return tx.Commit()