Documentation
¶
Overview ¶
Package racersdk reads objects through a node-local Racer cache and serves cache misses from your own storage.
Racer runs on each node and exposes every cache as two Unix sockets under /run/racer/<cache>/. A Client reads objects through the client socket. An Origin, registered with ServeOrigin on the origin socket, supplies objects that no Racer in the cluster has cached yet. One process may do either or both.
Reading ¶
Create one Client per cache and share it:
client, err := racersdk.NewClient(racersdk.ClientConfig{Cache: "blobs"})
if err != nil {
return err
}
defer client.Close()
object, err := client.Get(ctx, racersdk.Request{Key: key, Metadata: "bucket/path"})
if err != nil {
return err
}
defer object.Close()
_, err = io.Copy(w, object)
Client.Get returns once Racer has accepted the read, with the object's Metadata available from Object.Metadata. Client.Stat returns metadata without reading content. Use ReadOptions to read a byte range or to pin a version by ETag.
A Key names an object whose content may change. Each version has its own ETag, and the bytes of a version never change. One Get reads a single version from start to end, but two separate reads of the same key may see different versions. To read the same version more than once, take the ETag from Client.Stat or Object.Metadata and pass it in ReadOptions.
Copy or no copy ¶
An Object can be consumed in exactly two ways. Pick one per object.
Object.Read copies. Each call copies bytes from the socket into your buffer, so your code sees and owns the data: use it to hash, parse, decompress, or transform the content. Copying costs CPU and memory bandwidth proportional to the object size. Pass buffers of 32 KiB or more.
Object.WriteTo does not copy. It forwards the object to a writer. When the writer is backed by a file descriptor and implements io.ReaderFrom, such as an *os.File, a net.Conn, or the http.ResponseWriter of a plain-text HTTP/1 server, the kernel moves the data from Racer's socket to the destination with splice(2) and it never enters process memory. Other writers receive data through a single reused 256 KiB buffer. Your code never sees the bytes, so this is the right choice for proxies and anything that only stores or forwards content. io.Copy uses WriteTo automatically when the source is an Object, so io.Copy(dst, object) is the no-copy path; wrapping the object in another reader, such as io.TeeReader, switches to Read.
Both paths withhold the last byte until Racer confirms the whole range was delivered intact. A read that is cut short therefore always ends in an error rather than a silently truncated result, even for a destination that has already received the rest of the data.
Serving cache misses ¶
Implement Origin and pass it to ServeOrigin. Racer calls the origin for metadata and for individual pages of up to PageSize bytes. Requests for later pages are pinned to the ETag the origin returned for the first one, so origins only need ranged reads of a fixed version. The SDK validates every response, so a short, long, or mismatched body is reported to Racer instead of being cached.
Errors ¶
Test errors with errors.Is. For Object.WriteTo, check ErrDestination before the other sentinels. It marks a writer failure, so do not retry the Racer read or count it against Racer. The error wraps the writer's own error, which may also match SDK sentinels from a nested Racer read.
Otherwise, test against ErrNotFound, ErrUnauthorized, ErrForbidden, ErrVersionMismatch, ErrRangeNotSatisfiable, ErrUnavailable, and ErrInvalidRequest. Errors caused by an ending context wrap context.Canceled or context.DeadlineExceeded, and calls after Close wrap net.ErrClosed. A WriteTo timeout from the destination's ReadFrom cannot be attributed to either endpoint. It wraps os.ErrDeadlineExceeded and matches neither ErrDestination nor ErrUnavailable. Any other error means Racer or an origin misbehaved; a proxy should report it as a bad gateway. Origins report failures by wrapping the same errors.
Testing ¶
Package github.com/Azure/unbounded/pkg/racersdk/racersdktest runs an in-process Racer for tests, connecting a real Client to your Origin.
Index ¶
- Constants
- Variables
- func ServeOrigin(ctx context.Context, config OriginConfig, origin Origin) error
- type Client
- func (c *Client) Close() error
- func (c *Client) Get(ctx context.Context, request Request, options ...ReadOptions) (*Object, error)
- func (c *Client) Stat(ctx context.Context, request Request) (Metadata, error)
- func (c *Client) Warm(ctx context.Context, request Request, ttl time.Duration, opts WarmOptions) (WarmResult, error)
- type ClientConfig
- type Key
- type Metadata
- type Object
- type Origin
- type OriginConfig
- type OriginRequest
- type ReadOptions
- type Request
- type WarmOptions
- type WarmResult
Examples ¶
Constants ¶
const PageSize = 16 << 20
PageSize is the unit in which Racer caches and transfers objects (16 MiB). Objects no larger than PageSize may be read with ReadOptions.SmallObject.
Variables ¶
var ( // ErrInvalidRequest reports an invalid key, request value, option, or // configuration. ErrInvalidRequest = errors.New("racersdk: invalid request") ErrUnauthorized = errors.New("racersdk: unauthorized") // ErrForbidden reports that the origin denied access. ErrForbidden = errors.New("racersdk: forbidden") // ErrNotFound reports that the object does not exist. ErrNotFound = errors.New("racersdk: not found") // ErrVersionMismatch reports that the requested ETag is no longer // available. ErrVersionMismatch = errors.New("racersdk: version mismatch") // ErrRangeNotSatisfiable reports a range outside the object. ErrRangeNotSatisfiable = errors.New("racersdk: range not satisfiable") // overloaded or unreachable, or a transfer was cut short. Retrying may // succeed. ErrUnavailable = errors.New("racersdk: unavailable") // ErrDestination reports that the writer passed to [Object.WriteTo] // failed, for example because a downstream client disconnected, a disk // is full, or a write timed out. Racer and the origin are not at fault, // and retrying the read does not help unless the destination recovers. // Check ErrDestination before other sentinels: the error also wraps the // writer's own error, which may match SDK sentinels from a nested Racer read. // Timeouts from the destination's ReadFrom remain ambiguous and match // neither ErrDestination nor [ErrUnavailable]. ErrDestination = errors.New("racersdk: destination failed") )
Errors returned by Client methods and Object reads, and, except for ErrDestination, recognized when returned by an Origin. Test for them with errors.Is.
Other failures wrap the cause: context.Canceled or context.DeadlineExceeded when a context ends, and net.ErrClosed after Client.Close or Object.Close. When Object.WriteTo times out and cannot tell whether Racer or the destination stalled, the error wraps os.ErrDeadlineExceeded and matches neither ErrUnavailable nor ErrDestination. Anything else is a protocol or origin failure that callers usually report as a bad gateway.
Functions ¶
func ServeOrigin ¶
func ServeOrigin(ctx context.Context, config OriginConfig, origin Origin) error
ServeOrigin serves origin to Racer until ctx is canceled, then closes all connections and bodies and returns ctx.Err(). It does not wait for callbacks that ignore cancellation. On return it removes the socket it created. A process that exits without returning leaves the socket behind; see OriginConfig.RecoverStaleSocket.
ServeOrigin does not need root and does not create directories. The socket directory /run/racer/<cache>/origin must already exist, be owned by the effective user, and not be group or other writable. The Racer dataplane creates it owned by UID/GID 65532 with mode 0700, so origins must run as that user. ServeOrigin creates the socket there with mode 0600, owned by its effective user. The privileged dataplane connects to it through CAP_DAC_OVERRIDE.
Example ¶
An origin serving a single in-memory object.
package main
import (
"bytes"
"context"
"io"
"log"
"time"
"github.com/Azure/unbounded/pkg/racersdk"
)
var key, _ = racersdk.ParseKey("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")
func main() { //nolint:testableexamples // Requires a provisioned Racer origin directory.
origin := exampleMemoryOrigin([]byte("hello, racer"))
if err := racersdk.ServeOrigin(context.Background(), racersdk.OriginConfig{Cache: "cache"}, origin); err != nil {
log.Fatal(err)
}
}
func exampleMemoryOrigin(content []byte) racersdk.Origin {
return func(_ context.Context, r racersdk.OriginRequest) (racersdk.Metadata, io.ReadCloser, error) {
metadata := racersdk.Metadata{
Size: int64(len(content)),
ETag: `"v1"`,
ExpiresAt: time.Now().Add(time.Hour),
}
if r.Key != key {
return racersdk.Metadata{}, nil, racersdk.ErrNotFound
}
if r.ETag != "" && r.ETag != metadata.ETag {
return racersdk.Metadata{}, nil, racersdk.ErrVersionMismatch
}
if r.Head {
return metadata, nil, nil
}
start := min(r.Offset, metadata.Size)
length := min(r.Length, metadata.Size-start)
if length == 0 {
return metadata, nil, nil
}
return metadata, io.NopCloser(bytes.NewReader(content[start : start+length])), nil
}
}
Output:
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client reads objects from a Racer cache over its local Unix socket. It is safe for concurrent use; create one per cache and share it.
func NewClient ¶
func NewClient(config ClientConfig) (*Client, error)
NewClient returns a client for config.Cache. It does not connect until the first request, so Racer need not be running yet.
func (*Client) Close ¶
Close closes idle connections and fails in-progress and future calls with an error wrapping net.ErrClosed. It is safe to call more than once.
func (*Client) Get ¶
Get starts reading an object and returns once Racer has accepted the read and reported the object's metadata. Read the contents with Object.Read or Object.WriteTo and always call Object.Close.
Pass at most one ReadOptions. ctx applies to the whole transfer, not just the call to Get. However, Object.WriteTo cannot interrupt a blocked destination write unless the writer supports write deadlines.
func (*Client) Stat ¶
Stat returns the metadata of the current version of an object without reading its contents. Pass the returned ETag to ReadOptions to read exactly that version later.
Example ¶
A later range of a version found earlier, pinned by its ETag.
package main
import (
"context"
"errors"
"io"
"log"
"os"
"github.com/Azure/unbounded/pkg/racersdk"
)
var key, _ = racersdk.ParseKey("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")
func main() { //nolint:testableexamples // Requires a running Racer cache.
client, err := racersdk.NewClient(racersdk.ClientConfig{Cache: "cache"})
if err != nil {
log.Fatal(err)
}
defer client.Close()
ctx := context.Background()
metadata, err := client.Stat(ctx, racersdk.Request{Key: key})
if err != nil {
log.Fatal(err)
}
object, err := client.Get(ctx, racersdk.Request{Key: key}, racersdk.ReadOptions{
Offset: metadata.Size / 2,
ETag: metadata.ETag,
})
if errors.Is(err, racersdk.ErrVersionMismatch) {
log.Fatal("object changed since Stat")
} else if err != nil {
log.Fatal(err)
}
defer object.Close()
if _, err := io.Copy(os.Stdout, object); err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) Warm ¶ added in v0.16.0
func (c *Client) Warm(ctx context.Context, request Request, ttl time.Duration, opts WarmOptions) (WarmResult, error)
Warm asks Racer to load and retain an object without returning its contents. ttl must be positive and is rounded up to whole seconds. A duration whose rounded value overflows time.Duration is invalid. A zero RetainUntil means Racer refused retention; metadata is still returned. Warm shares Stat's lane. Pages are held on each page's primary owner in the cache cluster, which is not always the local node. Reads on this node wait for warm progress.
type ClientConfig ¶
type ClientConfig struct {
// Cache names the Racer cache, a DNS subdomain such as "blobs". The
// client connects to /run/racer/<Cache>/client/socket.
Cache string
// MaxConnections bounds concurrent [Client.Get] transfers without
// [ReadOptions.SmallObject]. Zero means 64. SmallObject transfers have a
// separate four-slot lane. [Client.Stat] and [Client.Warm] share another
// lane with MaxConnections slots, so every transfer can be preceded by a
// Stat. Each lane has a bounded queue; calls fail with [ErrUnavailable] if
// it is full or the wait times out.
MaxConnections int
// PageCredits is how many pages Racer may send ahead of the reader on
// each [Client.Get]. Racer's fetch window for a read is the smaller of
// this and its RACER_RANGE_WINDOW_PAGES, so it bounds single-read
// throughput. Zero means 2; the maximum is 64. Each credit lets Racer hold
// one more 16 MiB page for the read.
PageCredits int
}
ClientConfig configures a Client.
type Key ¶
type Key [32]byte
Key names an object in a Racer cache. The object's content may change over time; each version is identified by the ETag in its Metadata and must never change. A key is often a content digest, such as the SHA-256 of a blob, in which case the bytes cannot change, but the origin may report a new ETag for the same bytes.
type Metadata ¶
type Metadata struct {
// Size is the object length in bytes.
Size int64
// ETag is a strong HTTP entity tag such as "\"v1\"". Racer uses it to
// ensure every page of a read comes from the same version.
ETag string
// ContentType is optional.
ContentType string
// ExpiresAt is when Racer must stop serving this version from cache. It
// is required and kept at millisecond precision.
ExpiresAt time.Time
}
Metadata describes one version of an object. The bytes of a version, identified by its ETag, must never change.
type Object ¶
type Object struct {
// contains filtered or unexported fields
}
Object is an in-progress read returned by Client.Get. Consume it with either Object.Read or Object.WriteTo and always call Object.Close.
Read copies data into your buffer, so you can inspect, hash, or transform it. WriteTo hands data straight to a destination and, when the destination is backed by a file descriptor, never copies it into process memory. Prefer WriteTo whenever you only forward bytes; see the package documentation for details.
Both methods withhold the last byte of the read until Racer confirms the transfer completed, so a successful read is never silently truncated.
One goroutine at a time may call Read or WriteTo. Metadata and Close may be called concurrently with them.
func (*Object) Close ¶
Close stops the read and closes its Racer connection. It is safe to call more than once and concurrently with Read or WriteTo. Close does not wait for those calls to return and always returns nil. An interrupted Read or WriteTo returns an error wrapping net.ErrClosed.
If WriteTo is already blocked in a destination write without supported write deadlines, that write must return on its own before WriteTo can return.
func (*Object) Metadata ¶
Metadata describes the version being read. Size is the whole object, not the selected range.
func (*Object) Progress ¶ added in v0.19.0
Progress returns how many bytes of the object Read and WriteTo have handed to the caller so far. It is safe to call from any goroutine, including while WriteTo runs, so callers can report progress on long reads.
func (*Object) Read ¶
Read copies the next bytes of the object into p. It returns io.EOF once the whole selected range has been read and verified complete.
Every byte is copied from the kernel into p. Reads smaller than 4 KiB are staged through an internal buffer and copied twice; use buffers of 32 KiB or more for throughput.
Example ¶
Read copies the object into your buffer, for when you need to look at the bytes, here to hash them.
package main
import (
"context"
"crypto/sha256"
"fmt"
"io"
"log"
"github.com/Azure/unbounded/pkg/racersdk"
)
var key, _ = racersdk.ParseKey("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")
func main() { //nolint:testableexamples // Requires a running Racer cache.
client, err := racersdk.NewClient(racersdk.ClientConfig{Cache: "cache"})
if err != nil {
log.Fatal(err)
}
defer client.Close()
object, err := client.Get(context.Background(), racersdk.Request{Key: key})
if err != nil {
log.Fatal(err)
}
defer object.Close()
hash := sha256.New()
if _, err := io.CopyBuffer(hash, object, make([]byte, 256<<10)); err != nil {
log.Fatal(err)
}
fmt.Printf("%x\n", hash.Sum(nil))
}
Output:
func (*Object) WriteTo ¶
WriteTo writes the rest of the object to w and returns the number of bytes written. io.Copy calls it automatically. A nil error means the whole selected range was written and Racer confirmed it complete. A failure from w.Write, including a short write or a write timeout, matches ErrDestination and wraps w's error. A timeout from w.ReadFrom cannot be attributed to either endpoint: it wraps os.ErrDeadlineExceeded and matches neither ErrDestination nor ErrUnavailable.
If w implements io.ReaderFrom and is backed by a file descriptor, such as a *net.TCPConn, an *os.File, or an HTTP/1 http.ResponseWriter, data moves from Racer's socket to w inside the kernel with splice(2), with no copy into process memory. Other writers receive data through a reused 256 KiB buffer.
When w supports SetWriteDeadline, directly or through an HTTP response controller, WriteTo takes over w's write deadline for the whole call. It replaces any deadline already set on w, including one derived from http.Server.WriteTimeout, with a fixed timeout for each write, and moves it to the present to interrupt a blocked write if the Get context ends. Before returning, it clears the deadline, so w has no write deadline afterward. To keep an overall write budget, bound the Get context and set the deadline again after WriteTo returns. WriteTo never closes w. Set HTTP response headers, including Content-Length, before calling WriteTo. Without supported write deadlines, neither context cancellation nor Object.Close can interrupt an already-blocked write to w; that write must return on its own.
Example ¶
WriteTo forwards the object without copying it through your process, here to an HTTP response.
package main
import (
"errors"
"log"
"net/http"
"strconv"
"github.com/Azure/unbounded/pkg/racersdk"
)
var key, _ = racersdk.ParseKey("2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae")
func main() { //nolint:testableexamples // Requires a running Racer cache.
client, err := racersdk.NewClient(racersdk.ClientConfig{Cache: "cache"})
if err != nil {
log.Fatal(err)
}
defer client.Close()
http.HandleFunc("/blob", func(w http.ResponseWriter, r *http.Request) {
object, err := client.Get(r.Context(), racersdk.Request{Key: key})
if errors.Is(err, racersdk.ErrNotFound) {
http.NotFound(w, r)
return
} else if err != nil {
http.Error(w, "unavailable", http.StatusBadGateway)
return
}
defer object.Close()
w.Header().Set("Content-Length", strconv.FormatInt(object.Metadata().Size, 10))
if _, err := object.WriteTo(w); err != nil {
// Headers are sent; abort so the client sees a truncated body.
panic(http.ErrAbortHandler)
}
})
}
Output:
type Origin ¶
type Origin func(ctx context.Context, request OriginRequest) (Metadata, io.ReadCloser, error)
Origin loads objects for Racer on a cache miss. It is called concurrently, once per request, and must honor ctx.
For a metadata request (Head is set) return the current metadata and a nil body. Otherwise return the metadata of the version you are serving and a body that yields exactly the bytes in [Offset, min(Offset+Length, Size)). When that range is empty, return a nil body. If ETag is set, serve only that version and return ErrVersionMismatch if it is gone; returning metadata with a different ETag has the same effect.
Report failures by wrapping one of the package errors, for example fmt.Errorf("%w: %w", racersdk.ErrNotFound, err). Any other error is reported to Racer as an internal error.
The SDK takes ownership of a non-nil body even when err is non-nil, and closes it exactly once, possibly concurrently with Read to abort a transfer. Panics are recovered.
type OriginConfig ¶
type OriginConfig struct {
// Cache names the Racer cache. The origin listens on
// /run/racer/<Cache>/origin/socket. The directory must already exist and
// its ancestors must not be symlinks.
Cache string
// CacheUID optionally binds the origin to one ClusterCache UID. Origin
// sockets are named by cache name, so a ClusterCache that is deleted and
// recreated under the same name reuses the path while an origin for the
// old UID may still be listening. When CacheUID is set, requests whose
// Racer-Cache header names a different or no UID get 503 Service
// Unavailable instead of being served. Empty accepts any cache UID.
CacheUID string
// MaxConcurrentRequests bounds concurrent content requests; Racer receives
// a retryable error beyond it. Zero means 64. Metadata requests have a
// small separate limit.
MaxConcurrentRequests int
// RecoverStaleSocket lets ServeOrigin replace a socket left behind by an
// earlier ServeOrigin with this option that exited without cleanup, such
// as after a crash or SIGKILL. Enable it when a supervisor restarts the
// origin. The directory must be owned by this process's effective user
// and must not be writable by group or others. ServeOrigin keeps
// .racer-origin.lock and .racer-origin.socket in the directory to
// recognize its own sockets, and it never replaces a socket that is
// still accepting connections or that it did not create with this
// option. Turning the option on after a crash with it off therefore
// does not remove the old socket.
//
// Without it, an existing socket path is an error. ServeOrigin removes
// its socket only when it returns, so if the process exits without
// returning, later calls fail until the socket is removed by hand or by
// a startup step that knows no other origin is serving.
RecoverStaleSocket bool
}
OriginConfig configures ServeOrigin.
type OriginRequest ¶
type OriginRequest struct {
Request
// Head asks for metadata only.
Head bool
// ETag, when set, is the only version that may be served.
ETag string
// Offset is the first byte to return. It is always a multiple of
// [PageSize].
Offset int64
// Length is the maximum number of bytes to return, at most [PageSize].
// The range may extend past the end of the object.
Length int64
}
OriginRequest is a request from Racer to an Origin. The embedded Request carries the key and the opaque values a client passed to Client.Get or Client.Stat.
type ReadOptions ¶
type ReadOptions struct {
// Offset is the first byte to read.
Offset int64
// Length is the number of bytes to read. Zero reads to the end of the
// object. A range that extends past the end fails with
// [ErrRangeNotSatisfiable].
Length int64
// ETag pins the read to one version, typically from [Client.Stat]. Use it
// when several reads of a key must see the same bytes, since separate
// unpinned reads may see different versions. If that version is no
// longer available, Get fails with [ErrVersionMismatch].
ETag string
// SmallObject declares that the object is at most [PageSize] bytes. Small
// reads use a separate admission queue so they are not delayed behind
// large transfers. Get fails with [ErrInvalidRequest] if the object is
// larger.
SmallObject bool
}
ReadOptions selects part of an object or a specific version. The zero value reads the whole current version.
type Request ¶
type Request struct {
Key Key
// Metadata tells the origin how to locate the object, for example a bucket
// and path. Racer does not interpret it.
Metadata string
// Authorization is a credential for the origin. Racer does not interpret it.
Authorization string
}
Request identifies an object and carries the opaque values that Racer forwards unchanged to the Origin on a cache miss.
Metadata and Authorization may be empty; otherwise, each is limited to 8 KiB with no leading or trailing ASCII space (0x20), bytes below 0x20, or byte 0x7f. Bytes above 0x7f are allowed; values need not be valid UTF-8. Formatting a Request with the fmt package redacts both values.
type WarmOptions ¶ added in v0.16.0
type WarmOptions struct {
// ETag pins warming to this strong entity tag. An unavailable version fails
// with [ErrVersionMismatch].
ETag string
}
WarmOptions selects the version to warm. The zero value selects the current version.
type WarmResult ¶ added in v0.16.0
type WarmResult struct {
Metadata Metadata
// RetainUntil is the accepted deadline. Zero means retention was refused.
// Acceptance does not mean that all pages have finished loading.
RetainUntil time.Time
}
WarmResult reports the selected version and whether retention was accepted.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
fakeracer
Package fakeracer implements a sequential, noncaching Racer test daemon.
|
Package fakeracer implements a sequential, noncaching Racer test daemon. |
|
sdkhook
Package sdkhook connects racersdktest to private SDK construction seams.
|
Package sdkhook connects racersdktest to private SDK construction seams. |
|
wire
Package wire implements Racer HTTP heads, metadata grammar, and subscription framing.
|
Package wire implements Racer HTTP heads, metadata grammar, and subscription framing. |
|
Package racersdktest runs an in-process fake Racer so tests can exercise a real racersdk.Client and racersdk.Origin without a deployed Racer.
|
Package racersdktest runs an in-process fake Racer so tests can exercise a real racersdk.Client and racersdk.Origin without a deployed Racer. |