Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Stream run logs over the runner's SSH channel

5ece70a50ebda392626b92bdf29d28bc9b444954 · Keenan Nemetz · 10d ago · parent fdf2340f

Stream run logs over the runner's SSH channel

The runner wrote the log to a temp file and uploaded it as one LFS object
when the job ended, so a running job had no log to read and a log was stored
like a shared artifact even though it belongs to one attempt.

The runner sends 128 KiB chunks over the same runner channel that holds the
lease. The server appends them to data/logs/<repo>/<run>/<attempt>.log and
records the size while the run is still running. The finish record carries
the final size; the server checks the file length, syncs it, and stores the
result. A partial or failed stream leaves no log on the record.

Logs are no longer LFS objects: they are not shared, not content-addressed,
and the web serves the file directly, with range and step slices. GC no
longer treats them as LFS roots.

server/integration_test.go +11−16

@@ -5,7 +5,6 @@ import (
5 5 "bytes"
6 6 "crypto/ed25519"
7 7 "crypto/rand"
8 "crypto/sha256"
9 8 "encoding/pem"
10 9 "errors"
11 10 "fmt"
@@ -2827,21 +2826,19 @@ jobs:
2827 2826 if done.Status != runs.StatusSuccess {
2828 2827 t.Fatalf("run status = %s (exit %d)", done.Status, done.ExitCode)
2829 2828 }
2830 // The log is the runner's own object, not something the queue collected: the id
2831 // in the run is the hash of the bytes the runner uploaded, the queue holds no
2832 // text at all, and the page serves those bytes verbatim.
2833 if done.LogOID == "" {
2834 t.Fatalf("finished run has no archived log: %+v", done)
2835 }
2836 if done.Log != "" {
2837 t.Fatalf("the queue kept a copy of the log: %q", done.Log)
2829 // The log is a file under the instance's log directory, streamed while the job
2830 // ran rather than uploaded as one object at the end. The page serves those bytes.
2831 logPath := filepath.Join(f.cfg.LogsDir(), f.repo, done.ID, fmt.Sprintf("%d.log", done.Attempts))
2832 stored, err := os.ReadFile(logPath)
2833 if err != nil {
2834 t.Fatalf("finished run has no log file: %v", err)
2838 2835 }
2839 2836 code, body := f.get("/r/" + f.repo + "/runs/" + done.ID + "/log")
2840 2837 if code != 200 || !strings.Contains(body, "hello from the runner") {
2841 2838 t.Fatalf("run log page = %d, body:\n%s", code, body)
2842 2839 }
2843 if sum := fmt.Sprintf("%x", sha256.Sum256([]byte(body))); sum != done.LogOID {
2844 t.Fatalf("the served log hashes to %s, but the run names %s: the object is not the runner's bytes", sum, done.LogOID)
2840 if body != string(stored) {
2841 t.Fatalf("the served log differs from %s", logPath)
2845 2842 }
2846 2843
2847 2844 // The result is stored in the database, not on the branch: the record names this
@@ -2858,7 +2855,7 @@ jobs:
2858 2855 if err != nil || rec == nil {
2859 2856 t.Fatalf("no stored attempt: %v", err)
2860 2857 }
2861 if rec.Log == nil || rec.Log.OID != done.LogOID || rec.Exit != 0 || rec.Job != done.Job {
2858 if rec.LogSize != int64(len(stored)) || rec.Exit != 0 || rec.Job != done.Job {
2862 2859 t.Fatalf("the record does not describe the run: %+v", rec)
2863 2860 }
2864 2861 // The runner also records what each step did and where its output lives in the
@@ -3096,11 +3093,9 @@ jobs:
3096 3093 return r.Status == runs.StatusSuccess || r.Status == runs.StatusFailed
3097 3094 })
3098 3095 if done.Status != runs.StatusSuccess {
3099 // The log is an object the finished attempt uploaded; the attempt that was
3100 // killed never uploaded one, which is the cost of not streaming.
3101 3096 _, body := f.get("/r/" + f.repo + "/runs/" + done.ID + "/log")
3102 t.Fatalf("the retried run did not succeed: %s (exit %d, log object %q)\n%s",
3103 done.Status, done.ExitCode, done.LogOID, body)
3097 t.Fatalf("the retried run did not succeed: %s (exit %d)\n%s",
3098 done.Status, done.ExitCode, body)
3104 3099 }
3105 3100 if done.Attempts < 2 {
3106 3101 t.Fatalf("attempts = %d; the job was not actually handed to a second runner", done.Attempts)