Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
cf9191b
add start of cache tool packages, runner
bradfitz Apr 24, 2023
70bd57f
README.md: add
bradfitz Apr 24, 2023
474d34a
README.md: usage, examples
bradfitz Apr 24, 2023
dbcd5c9
add HTTP client+server support
bradfitz Apr 24, 2023
ff29b8f
fix an HTTP issue with multiple actionIDs sharing same 0-length outputID
bradfitz Apr 25, 2023
a05d6ad
chore: fix go version to 1.20
xieyuschen Apr 25, 2023
4505220
Fix go install cmd package path
0xack13 Apr 25, 2023
fdc86ae
Add `OutputID` for compatibility with Go1.24
j178 Dec 16, 2024
e608868
README.md: update
bradfitz Aug 9, 2025
6ecf2ab
cmd/gocached: add start of good server w/ sqlite indexes, etc
bradfitz Aug 9, 2025
063779f
cmd/gocached: add more metrics
bradfitz Aug 9, 2025
0271c5b
cmd/gocached: bump access time relatime-style on get
bradfitz Aug 10, 2025
ba6e502
cmd/gocached: add Prometheus /metrics handler
bradfitz Aug 10, 2025
8e09c0d
cmd/gocached: set busy timeout on sqlite
bradfitz Aug 10, 2025
f7da204
cachers: forest-ify disk cache like Go does
bradfitz Aug 10, 2025
418bac0
cmd/gocached: optimize storage of zero-sized objects
bradfitz Aug 10, 2025
91e5746
cmd/gocached: redo the schema to store blobs separately
bradfitz Aug 10, 2025
d50907c
cmd/gocached: store only blobs on disk by sha256
bradfitz Aug 10, 2025
3a129e3
cmd/gocached: start adding cleaning
bradfitz Aug 11, 2025
5adb8d8
cmd/gocached: do background cleaning
bradfitz Aug 11, 2025
0aa6668
cmd/gocached: add dup put and eviction metrics
bradfitz Aug 13, 2025
45703a8
cmd/gocached: add PutErr metric, serialize SQLite writes
bradfitz Aug 15, 2025
6f8051d
cmd/gocached: use sqliteWriteMu in another spot
bradfitz Aug 20, 2025
897165f
cmd/gocached: separate out debug port, add pprof info
bradfitz Sep 23, 2025
c642167
cachers: consume 404 response bodies to allow HTTP conn reuse
bradfitz Sep 24, 2025
eae2c3b
cachers,cmd/go-cacher-server: fix unexpected http 404s (bradfitz/go-t…
tomhjp Oct 17, 2025
504a808
cmd/gocached: tighten fs permissions and http errors (bradfitz/go-too…
tomhjp Nov 6, 2025
a456301
cachers,cmd/{go-cacher,gocached}: add JWT-based auth sessions
tomhjp Oct 28, 2025
7ab488e
{cmd/,}gocached: make gocached library package (bradfitz/go-tool-cach…
tomhjp Nov 13, 2025
4d13057
gocached: fix AccessTime bump query to use an index
bradfitz Dec 1, 2025
7fde5d9
gocached: batch access time updates, reducing SQLite write mutex cont…
bradfitz Dec 1, 2025
9402fa5
cachers: make HTTP client support lz4 decompression
bradfitz Feb 11, 2026
559ec15
cachers: fix disk cacher locking on Windows
tomhjp Feb 11, 2026
559267a
cachers: buffer HTTPClient.Put body before writing to Disk + Network
bradfitz Feb 11, 2026
8862218
.github/workflows: add CI for linux+mac+windows, fix Windows test fai…
bradfitz Feb 12, 2026
27b43ea
cachers: add HTTPClient.BestEffortHTTP
bradfitz Feb 13, 2026
8fd5a47
gocached: compress bigger blobs on disk (lz4)
bradfitz Feb 11, 2026
50cdce2
cachers: suppress 401 and 403 error logs for best-effort (bradfitz/go…
tomhjp Feb 16, 2026
4a8e70b
gocached: use prepared statement for atime updates (bradfitz/go-tool-…
tomhjp Feb 18, 2026
e0d4185
gocached: allow multiple JWT issuers (bradfitz/go-tool-cache#33)
tomhjp Mar 3, 2026
3c56425
cmd/gocached: add parentdeath support
bradfitz Mar 15, 2026
9e96b49
gocached: bound WAL size and expose db/wal size gauges
bradfitz Jun 1, 2026
a4f4925
gocached: do per-shard usage stats and incremental cleanup
bradfitz Jun 2, 2026
e4be1fd
gocached: support namespaces (bradfitz/go-tool-cache#34)
tomhjp Jun 12, 2026
3eff7ec
gocached: add get/post latency histogram metrics
bradfitz Jun 24, 2026
2e1c5c7
gocached: add size histogram and more labels to latency histograms (b…
tomhjp Jun 25, 2026
395850e
gocached, cmd/gocached: add storage tiering and a separate SQLite dir
bradfitz Jul 10, 2026
171d8d8
gocached: do both PUT metadata inserts in one SQLite transaction
bradfitz Jul 22, 2026
c221261
cachers,cmd/go-cacher: make HTTP PUTs async for BestEffortHTTP (bradf…
tomhjp Jul 22, 2026
f7c18ef
gocached: extract PUT insert and GET response helpers
bradfitz Jul 22, 2026
b315aaa
gocached: skip dot-directories in the hot tier startup scan
bradfitz Jul 22, 2026
265213c
gocached: add the put-queue core: pending map, backpressure, spooling
bradfitz Jul 22, 2026
d3a92a9
gocached: add the put-queue background movers and metadata flusher
bradfitz Jul 22, 2026
f80e406
gocached: make PUTs asynchronous via the put-queue
bradfitz Jul 22, 2026
8fb4c1d
gocached: add PUT benchmarks and document the async pipeline
bradfitz Jul 22, 2026
5e05c12
gocached: bound PUT body reads by a minimum acceptable upload rate
bradfitz Jul 22, 2026
aab5367
gocached: sweep blobs orphaned between the blob copy and the commit
bradfitz Jul 22, 2026
7fb416a
cachers,cmd/go-cacher: add stats and variable timeouts for async PUTs…
tomhjp Jul 23, 2026
a538d76
gocached: split namespace mapping into read/write grants (bradfitz/go…
tomhjp Jul 24, 2026
fc019da
gocached,cmd/gocached: add global namespace generations for cheap wipes
bradfitz Aug 4, 2026
48743dc
gocached: extend JWKS support for AWS STS issuers (bradfitz/go-tool-c…
tomhjp Aug 5, 2026
237103e
gocached: allow square brackets in namespaces (bradfitz/go-tool-cache…
tomhjp Aug 20, 2026
85f4c49
cachers: set the executable bit on disk cache output files
bradfitz Sep 9, 2026
16d2cf5
gocached: instrument the put-queue pipeline stages and caps
bradfitz Sep 18, 2026
9c0e653
gocached: create main blob shard dirs only when a rename says they're…
bradfitz Sep 18, 2026
e3fc37e
gocached: give inline PUTs their own put-queue lane
bradfitz Sep 18, 2026
81d442c
gocached: derive put-queue caps from the host and memory budgets
bradfitz Sep 18, 2026
c8c9163
all: merge bradfitz/go-tool-cache history as gocache and cmd/gocached
bradfitz Oct 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions cmd/gocached/gocached.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// The gocached command is a small example binary showing how to use
// the gocached library package.
package main

import (
"context"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"strings"
"time"

"github.com/bradfitz/parentdeath"
"github.com/tailscale/tb/gocache/gocached"
_ "modernc.org/sqlite"
)

var (
dir = flag.String("cache-dir", "", "cache directory, if empty defaults to <UserCacheDir>/gocached")
sqliteDir = flag.String("sqlite-dir", "", "directory for the SQLite metadata database; if empty, defaults to the cache directory")
hotDir = flag.String("hot-dir", "", "if non-empty, enable storage tiering with this directory as a fast tier (e.g. local NVMe) holding a bounded copy of recently used blobs; the cache directory remains the source of truth")
hotCapacity = flag.Int("hot-capacity-gb", 600, "maximum size of the hot tier directory in GiB; only used with --hot-dir")
putSpoolCap = flag.Int("put-spool-gb", 0, "capacity in GiB of the put queue's spooled lane: accepted PUT bytes waiting on local disk (the hot tier's filesystem, with --hot-dir) for a copy into the cache directory; 0 means one eighth of that filesystem's size")
verbose = flag.Bool("verbose", false, "be verbose")
listen = flag.String("listen", ":31364", "listen address for the build-facing HTTP server")
debugListen = flag.String("debug-listen", "", "if non-empty, listen address for the debug HTTP server (pprof, metrics, etc)")

maxSize = flag.Int("max-size-gb", 50, "maximum size of the cache in GiB; 0 means no limit")
maxAge = flag.Int("max-age-days", 60, "maximum age of objects in the cache in days; 0 means no limit")

globalGeneration = flag.Int("global-generation", 1, "generation number of the global namespace; incrementing it effectively wipes the global namespace, with the old generation's objects aging out via normal LRU eviction")

shardPrefixLen = flag.Int("shard-prefix-len", 2, "number of SHA256 hex characters per usage-stats shard key (valid 1..4); total shards = 16^n. Defaults to 2 (256 shards). Increase if a single shard's stats scan becomes too slow on a large DB")

jwtIssuer = flag.String("jwt-issuer", "", "the issuer to trust JWTs from; if set, all requests will require auth, and must set at least one -jwt-claim")
// See example GitHub token claims for what can be available:
// https://docs.github.com/en/actions/concepts/security/openid-connect
jwtClaims = make(jwtClaimValue)
globalJWTClaims = make(jwtClaimValue)
)

type jwtClaimValue map[string]string

func (v jwtClaimValue) String() string {
return fmt.Sprintf("%v", map[string]string(v))
}

func (v jwtClaimValue) Set(s string) error {
claim, value, ok := strings.Cut(s, "=")
if !ok || claim == "" || value == "" {
return fmt.Errorf("bad claim %q, want x=y", s)
}
v[claim] = value
return nil
}

func main() {
flag.Var(&jwtClaims, "jwt-claim", "a claim in the form x=y that any JWT presented must have to start a session; may be specified more than once")
flag.Var(&globalJWTClaims, "global-jwt-claim", "an additional claim in the form x=y that a JWT must have to allow writing to the cache's global namespace; may be specified more than once")
flag.Parse()

parentdeath.Monitor(func() {
log.Printf("gocached: parent process died, exiting")
os.Exit(0)
})

opts := []gocached.ServerOption{
gocached.WithDir(*dir),
gocached.WithSQLiteDir(*sqliteDir),
gocached.WithVerbose(*verbose),
gocached.WithMaxSize(int64(*maxSize) << 30),
gocached.WithMaxAge(time.Duration(*maxAge) * 24 * time.Hour),
gocached.WithShardPrefixLen(*shardPrefixLen),
gocached.WithGlobalGeneration(*globalGeneration),
}

if *hotDir != "" {
if *hotCapacity <= 0 {
log.Fatal("--hot-capacity-gb must be positive when --hot-dir is set")
}
opts = append(opts,
gocached.WithHotDir(*hotDir),
gocached.WithHotCapacity(int64(*hotCapacity)<<30),
)
}
if *putSpoolCap > 0 {
opts = append(opts, gocached.WithPutSpoolCapacity(int64(*putSpoolCap)<<30))
}

if *jwtIssuer != "" {
if len(jwtClaims) == 0 {
log.Fatal("must specify --jwt-claim at least once when --jwt-issuer is set")
}

opts = append(opts,
gocached.WithJWTAuth(*jwtIssuer),
gocached.WithNamespaceMapping(func(ctx context.Context, claims map[string]any) (*gocached.NamespaceGrant, error) {
var ns gocached.Namespace
for k, want := range jwtClaims {
if got := claims[k]; got != want {
return nil, fmt.Errorf("claim %q = %v, want %v", k, got, want)
}
if ns != "" {
ns += ","
}
ns += gocached.Namespace(fmt.Sprintf("%s=%s", k, want))
}
for k, want := range globalJWTClaims {
if got := claims[k]; got != want {
return &gocached.NamespaceGrant{
WriteNamespace: &ns,
ExtraReadNamespace: &ns,
}, nil
}
}
return &gocached.NamespaceGrant{
WriteNamespace: new(gocached.GlobalNamespace),
ExtraReadNamespace: &ns,
}, nil
}),
)
}

srv, err := gocached.NewServer(opts...)
if err != nil {
log.Fatalf("starting gocached: %v", err)
}

if *debugListen != "" {
debugLn, err := net.Listen("tcp", *debugListen)
if err != nil {
log.Fatalf("debug listen: %v", err)
}
go func() {
log.Fatal(http.Serve(debugLn, http.HandlerFunc(srv.ServeHTTPDebug)))
}()
}

log.Printf("gocached: listening on %s ...", *listen)
log.Fatal(http.ListenAndServe(*listen, srv))
}
59 changes: 59 additions & 0 deletions cmd/gocached/gocached_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

package main

import (
"testing"

"github.com/google/go-cmp/cmp"
)

func TestJWTClaimFlag(t *testing.T) {
for name, tc := range map[string]struct {
input []string
want jwtClaimValue
}{
"single_claim": {
input: []string{"role=builder"},
want: jwtClaimValue{"role": "builder"},
},
"multiple_claims": {
input: []string{"env=prod", "team=devops"},
want: jwtClaimValue{"env": "prod", "team": "devops"},
},
"duplicate_keys": {
input: []string{"key=value1", "key=value2"},
want: jwtClaimValue{"key": "value2"},
},
"value_with_equals": {
input: []string{"data=a=b=c"},
want: jwtClaimValue{"data": "a=b=c"},
},
} {
t.Run(name, func(t *testing.T) {
claim := make(jwtClaimValue)
for _, v := range tc.input {
if err := claim.Set(v); err != nil {
t.Fatalf("Set failed: %v", err)
}
}
if diff := cmp.Diff(claim, tc.want); diff != "" {
t.Errorf("jwtClaimValue mismatch (-got +want):\n%s", diff)
}
})
}
}

func TestInvalidJWTClaimFlags(t *testing.T) {
for _, s := range []string{
"invalidclaim",
"=nokey",
"novalue=",
} {
claim := make(jwtClaimValue)
if err := claim.Set(s); err == nil {
t.Fatalf("Set with invalid claim %q did not return error", s)
}
}
}
24 changes: 22 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,51 @@ module github.com/tailscale/tb
go 1.27.1

require (
github.com/bradfitz/parentdeath v0.0.0-20260315043412-764506aeb900
github.com/cespare/xxhash/v2 v2.3.0
github.com/go-jose/go-jose/v4 v4.1.3
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/go-cmp v0.7.0
github.com/pierrec/lz4/v4 v4.1.26
github.com/prometheus/client_golang v1.24.1
github.com/prometheus/client_model v0.6.3
golang.org/x/sync v0.23.0
modernc.org/sqlite v1.51.0
tailscale.com v1.103.0-pre.0.20260920011920-7d96cf5a62ef
)

require (
filippo.io/edwards25519 v1.2.0 // indirect
github.com/akutz/memconn v0.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/dblohm7/wingoes v0.0.0-20260526185140-fb298caac7ca // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fxamacker/cbor/v2 v2.9.3 // indirect
github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
github.com/jsimonetti/rtnetlink v1.4.2 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mdlayher/netlink v1.11.2 // indirect
github.com/mdlayher/socket v0.7.0 // indirect
github.com/mitchellh/go-ps v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/prometheus/common v0.71.0 // indirect
github.com/prometheus/procfs v0.21.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
github.com/x448/float16 v0.8.4 // indirect
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
go4.org/netipx v0.0.0-20260823151212-3075585bcbeb // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect
golang.org/x/net v0.59.0 // indirect
golang.org/x/sync v0.23.0 // indirect
golang.org/x/sys v0.48.0 // indirect
golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
google.golang.org/protobuf v1.36.12 // indirect
modernc.org/libc v1.72.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
Loading
Loading