package store import ( "context" "database/sql" "errors" "strings" "time" ) // Runner is one machine on one repository, because barerepo dispatches and records but never executes. type Runner struct { ID int64 Repo string Hostname string OS string Arch string Labels []string Attached time.Time LastSeen time.Time } // Offline reports a runner that has missed three polls. Chapter 15. func (r Runner) Offline(interval time.Duration) bool { return time.Since(r.LastSeen) > 3*interval } // AttachRunner keys on token and hostname, so restarting a machine leaves no second row. func (db *DB) AttachRunner(ctx context.Context, tokenID int64, repo, hostname, os, arch string, labels []string) (*Runner, error) { now := now() joined := strings.Join(labels, ",") var id int64 err := db.QueryRowContext(ctx, `SELECT id FROM runners WHERE token_id = ? AND hostname = ?`, tokenID, hostname).Scan(&id) switch { case errors.Is(err, sql.ErrNoRows): err = db.QueryRowContext(ctx, `INSERT INTO runners (token_id, repo, hostname, os, arch, labels, attached_at, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id`, tokenID, repo, hostname, os, arch, joined, now.Unix(), now.Unix()).Scan(&id) if err != nil { return nil, err } case err != nil: return nil, err default: if _, err := db.ExecContext(ctx, `UPDATE runners SET os = ?, arch = ?, labels = ?, last_seen = ? WHERE id = ?`, os, arch, joined, now.Unix(), id); err != nil { return nil, err } } return &Runner{ID: id, Repo: repo, Hostname: hostname, OS: os, Arch: arch, Labels: labels, Attached: now, LastSeen: now}, nil } // SeeRunner marks a runner as still there. func (db *DB) SeeRunner(ctx context.Context, id int64) error { _, err := db.ExecContext(ctx, `UPDATE runners SET last_seen = ? WHERE id = ?`, now().Unix(), id) return err } func (db *DB) Runner(ctx context.Context, id int64) (*Runner, error) { var r Runner var labels string var attached, seen int64 err := db.QueryRowContext(ctx, `SELECT id, repo, hostname, os, arch, labels, attached_at, last_seen FROM runners WHERE id = ?`, id). Scan(&r.ID, &r.Repo, &r.Hostname, &r.OS, &r.Arch, &labels, &attached, &seen) if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } if err != nil { return nil, err } r.Labels = splitLabels(labels) r.Attached = time.Unix(attached, 0) r.LastSeen = time.Unix(seen, 0) return &r, nil } // RunnersOf lists the machines attached to a repository. func (db *DB) RunnersOf(ctx context.Context, repo string) ([]Runner, error) { rows, err := db.QueryContext(ctx, `SELECT id, repo, hostname, os, arch, labels, attached_at, last_seen FROM runners WHERE repo = ? ORDER BY last_seen DESC`, repo) if err != nil { return nil, err } defer rows.Close() var out []Runner for rows.Next() { var r Runner var labels string var attached, seen int64 if err := rows.Scan(&r.ID, &r.Repo, &r.Hostname, &r.OS, &r.Arch, &labels, &attached, &seen); err != nil { return nil, err } r.Labels = splitLabels(labels) r.Attached = time.Unix(attached, 0) r.LastSeen = time.Unix(seen, 0) out = append(out, r) } return out, rows.Err() } // ForgetRunner drops a machine. Chapter 24: offline machines can be forgotten. func (db *DB) ForgetRunner(ctx context.Context, repo string, id int64) error { res, err := db.ExecContext(ctx, `DELETE FROM runners WHERE repo = ? AND id = ?`, repo, id) if err != nil { return err } if n, _ := res.RowsAffected(); n == 0 { return ErrNotFound } return nil } func splitLabels(s string) []string { if strings.TrimSpace(s) == "" { return nil } return strings.Split(s, ",") } // RunnersByToken maps one account's runner tokens to the machines that attached with them. func (db *DB) RunnersByToken(ctx context.Context, account string) (map[int64][]Runner, error) { rows, err := db.QueryContext(ctx, `SELECT r.token_id, r.id, r.repo, r.hostname, r.os, r.arch, r.labels, r.attached_at, r.last_seen FROM runners r JOIN tokens t ON t.id = r.token_id WHERE t.account = ? ORDER BY r.last_seen DESC`, account) if err != nil { return nil, err } defer rows.Close() out := map[int64][]Runner{} for rows.Next() { var r Runner var tokenID int64 var labels string var attached, seen int64 if err := rows.Scan(&tokenID, &r.ID, &r.Repo, &r.Hostname, &r.OS, &r.Arch, &labels, &attached, &seen); err != nil { return nil, err } r.Labels = splitLabels(labels) r.Attached = time.Unix(attached, 0) r.LastSeen = time.Unix(seen, 0) out[tokenID] = append(out[tokenID], r) } return out, rows.Err() } // RunnerWork is what a runner has done and whether it is doing something now. Chapter 24. type RunnerWork struct { Runs int Busy bool } // WorkOf counts the jobs each runner of a repository has taken, and finds the ones running now. func (db *DB) WorkOf(ctx context.Context, repo string) (map[int64]RunnerWork, error) { rows, err := db.QueryContext(ctx, `SELECT runner_id, COUNT(*), SUM(CASE WHEN state = ? THEN 1 ELSE 0 END) FROM jobs WHERE repo = ? AND runner_id IS NOT NULL GROUP BY runner_id`, JobRunning, repo) if err != nil { return nil, err } defer rows.Close() out := map[int64]RunnerWork{} for rows.Next() { var id int64 var runs, running int if err := rows.Scan(&id, &runs, &running); err != nil { return nil, err } out[id] = RunnerWork{Runs: runs, Busy: running > 0} } return out, rows.Err() }