The last flush waits at most a second, and not at all once a send has failed: an OTEL_EXPORTER_OTLP_ENDPOINT inherited from the desktop session may name a collector that isn't up.
- commit
- 902e724f8053380a05a2a336b5c1109b47ec5320
- parent
- 82eb79a0a0b3ee0071cdb1cad8cf68c9a9cac68e
- author
- Dami <dami.etoile@gmail.com>
| M | ROADMAP.md | 3 | +-- |
| M | docs/ | 12 | +++++++++-- |
| M | internal/ | 27 | ++++++++++++++++++----- |
| M | internal/ | 35 | ++++++++++++++++++++++++++++++ |
@@ -102,7 +102,7 @@ work left open. Sizes: S (a day or two), M (about a week), L (weeks).
102102 103103 | Item | Why | Size |104104 |---|---|---|105-| Open 012's XLSX output in real Excel and LibreOffice (a round-trip corpus, run by hand before releases) | The new writer is checked only by excelize and 012's reader | S |105+| Open 012's XLSX output in real Excel and LibreOffice (a round-trip corpus, run by hand before releases). In progress: a first sample is being checked in Excel | The new writer is checked only by excelize and 012's reader | S |106106 | Column and row formats travel with copy, cut and move; formulas reading blanks of a newly formatted column re-infer at once | Left open by step A | S |107107 | Save as onto another existing file asks before replacing (local and `012 serve`) | Only the open file is checked today | S |108108 | XLSX sheet names with characters Excel forbids: rewrite formulas that name them, not just the sheet | Formulas keep the old name today | S |@@ -110,7 +110,6 @@ work left open. Sizes: S (a day or two), M (about a week), L (weeks).
110110 | Precedent tracing explains when every precedent is on a hidden sheet | Says "no formula" today | S |111111 | Settings > JEV API key checks the key with a test call | Left open by the config work | S |112112 | `012 serve`: open a file from the ssh command line (`ssh -t host file.012` as a request, not exec), and save unsaved work to a recovery file on shutdown or idle timeout | Sessions start empty; unsaved work is lost today | M |113-| Decide whether 012 honors OTEL_* variables it inherits from the desktop session, or only its own config | A GUI-launched 012 exports to whatever the session's variables name | S |114113 115114 ### 2. Spreadsheet features Sheets users reach for116115 @@ -203,8 +203,16 @@ queues (2048 items per signal); a background goroutine sends them every
203203 2 seconds, or sooner once 512 are waiting, gzipped, with no retries. A204204 full queue drops new items and a failed request drops its batch; both205205 are counted in `o12.telemetry.dropped` and the `otlp` event on exit.206-On exit, what is queued is sent within one request timeout (so at most207-about 3 s with a collector that doesn't answer).206+On exit, what is queued is sent within a second at most, and not at all207+once a request has failed this session.208+209+012 honors the standard `OTEL_*` variables it inherits, like any210+OpenTelemetry program, unless its own config or flags override them. So211+if your desktop session sets `OTEL_EXPORTER_OTLP_ENDPOINT` for every app,212+012 exports there too; when that collector is gone (a laptop whose tailnet213+collector isn't up), sends fail in the background, are counted, and cost214+nothing at exit. Set `otlp-endpoint` in the config, or `OTEL_SDK_DISABLED=true`,215+to change that for 012 alone.208216 209217 `internal/telemetry/otlp_test.go` checks the payloads against the210218 spec's JSON mapping with a fake collector (hex ids, 64-bit integers as@@ -68,6 +68,7 @@ type exporter struct {
6868 sent [3]int64 // items accepted by the endpoint6969 dropped [3]int64 // items dropped because the queue was full7070 failed [3]int64 // items in requests that failed71+ down bool // the last request failed: the endpoint looks unreachable7172 7273 wake chan struct{}7374 done chan struct{}@@ -215,11 +216,18 @@ func (e *exporter) run() {
215216 case <-t.C:216217 case <-e.wake:217218 case <-e.done:218- // The last flush shares one timeout, so quitting 012 waits219- // at most that long for a collector that doesn't answer.220- ctx, cancel := context.WithTimeout(context.Background(), e.timeout)221- e.flush(ctx, true)222- cancel()219+ // Quitting waits at most finalTimeout for the last flush,220+ // and not at all when the endpoint already failed: an221+ // inherited OTEL_EXPORTER_OTLP_ENDPOINT may name a collector222+ // that's gone, and quitting shouldn't wait on it.223+ e.mu.Lock()224+ down := e.down225+ e.mu.Unlock()226+ if !down {227+ ctx, cancel := context.WithTimeout(context.Background(), e.finalTimeout())228+ e.flush(ctx, true)229+ cancel()230+ }223231 return224232 }225233 e.flush(context.Background(), false)@@ -232,10 +240,16 @@ func (e *exporter) shutdown() {
232240 close(e.done)233241 select {234242 case <-e.stopped:235- case <-time.After(e.timeout + time.Second):243+ case <-time.After(e.finalTimeout() + 250*time.Millisecond):236244 }237245 }238246 247+// finalTimeout bounds the flush when 012 quits.248+func (e *exporter) finalTimeout() time.Duration { return min(e.timeout, finalFlush) }249+250+// finalFlush is the most quitting waits for the last flush.251+const finalFlush = time.Second252+239253 func (e *exporter) flush(ctx context.Context, final bool) {240254 e.mu.Lock()241255 logs, spans, windows := e.logs, e.spans, e.windows@@ -269,6 +283,7 @@ func (e *exporter) send(ctx context.Context, sig, items int, body any) {
269283 url := [3]string{e.urls.logs, e.urls.traces, e.urls.metrics}[sig]270284 err := e.post(ctx, url, body)271285 e.mu.Lock()286+ e.down = err != nil272287 if err != nil {273288 e.failed[sig] += int64(items)274289 } else {@@ -7,6 +7,7 @@ import (
77 "io"88 "log/slog"99 "maps"10+ "net"1011 "net/http"1112 "net/http/httptest"1213 "os"@@ -362,6 +363,40 @@ func TestOTLPCloseIsBounded(t *testing.T) {
362363 }363364 }364365 366+// An endpoint that can't be reached (an inherited OTEL_* variable naming367+// a collector that's gone) costs nothing at exit once a send has failed.368+func TestOTLPCloseSkipsADeadEndpoint(t *testing.T) {369+ l, err := net.Listen("tcp", "127.0.0.1:0")370+ if err != nil {371+ t.Fatal(err)372+ }373+ dead := "http://" + l.Addr().String()374+ l.Close() // nothing listens there now375+ otlpSetup(t, OTLPConfig{Endpoint: dead, Timeout: 3 * time.Second})376+ for range batchSize {377+ Start("sort").End()378+ }379+ deadline := time.Now().Add(5 * time.Second)380+ for {381+ exp.mu.Lock()382+ down := exp.down383+ exp.mu.Unlock()384+ if down {385+ break386+ }387+ if time.Now().After(deadline) {388+ t.Fatal("the failed send was never noticed")389+ }390+ time.Sleep(10 * time.Millisecond)391+ }392+ Start("sort").End()393+ begin := time.Now()394+ Close()395+ if d := time.Since(begin); d > 200*time.Millisecond {396+ t.Errorf("Close after a failed send took %v", d)397+ }398+}399+365400 func TestOTLPOffSendsNothing(t *testing.T) {366401 c := newCollector(t)367402 t.Setenv("OTEL_EXPORTER_OTLP_ENDPOINT", c.srv.URL)