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.
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 immutable versions. 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 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. 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 ¶
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") )
Errors returned by Client methods and Object reads, and 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. 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. The socket is created with mode 0600, so the origin must run as the same user as the Racer dataplane, usually root. On return it removes the socket it created.
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 {
metadata := racersdk.Metadata{
Size: int64(len(content)),
ETag: `"v1"`,
ExpiresAt: time.Now().Add(time.Hour),
}
return func(_ context.Context, r racersdk.OriginRequest) (racersdk.Metadata, io.ReadCloser, error) {
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:
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 and
// [Client.Stat] calls each have a separate four-slot lane. Each lane has a
// bounded queue; calls fail with [ErrUnavailable] if it is full or the
// wait times out.
MaxConnections int
}
ClientConfig configures a Client. Only Cache is required.
type Key ¶
type Key [32]byte
Key names an immutable object in a Racer cache. It is usually a content digest, for example the SHA-256 of a blob.
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 immutable version of an object.
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) 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.
If w implements io.ReaderFrom and is backed by a file descriptor, such as a net.Conn, 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 bounds each write and interrupts it if the Get context ends. It clears its deadlines before returning and 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
// 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. The directory must be owned by this user and must
// not be writable by group or others. Without it, an existing socket
// path is an error.
RecoverStaleSocket bool
}
OriginConfig configures ServeOrigin. Only Cache is required.
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]. If the
// object has changed, 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.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
fakeracer
Package fakeracer implements a sequential, noncaching v2 Racer test daemon.
|
Package fakeracer implements a sequential, noncaching v2 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. |