Slap.Files (slap_files v0.4.0)

Copy Markdown View Source

Files stored with Slap.KV and an object store. A file lives at a ref, {partition, id}: a partition is a stable group of files (a document's, for example), listed together, and on one Slap.KV shard. Each file has one record in Slap.KV, which exists exactly while the file does, and a body stored in the record (inline) or as an object.

Files can be replaced; bodies never are. Each put/3 writes a new body, switches the file's record to it with a conditional write, and gives the file a new version. The old body is deleted after retention_ms, so a reader that already has it can finish.

Start it with the object store for bodies, after Slap.KV.Cluster:

{Slap.Files, store: {:url, "s3://bucket/prefix"}, path: "files"}

Options:

  • :store - required, as for Slap.SlateDB.open/2; :path - where the objects go in it (default "files").
  • :inline_max_bytes - bodies up to this size are stored inline with storage: :auto (default 16 KiB).
  • :inline_limit - the largest inline body, even with storage: :inline (default 1 MiB).
  • :retention_ms - how long an old body is kept (default 5 minutes).
  • :upload_timeout_ms - how long an upload may take before its object may be deleted (default 1 hour).
  • :sweep_interval_ms - see Slap.Files.Sweeper (default 10 s).
  • :max_clock_skew_ms - how far apart the nodes' clocks may be (default 30 s): see Slap.Files.Sweeper.
  • :reconcile_interval_ms - see Slap.Files.Sweeper (default 1 hour).
  • :name - instance name (default Slap.Files). Pass it as files: to file operations.
  • :cluster - the instance's KV cluster (default Slap.KV.Cluster); define an application-owned one with use Slap.KV.Cluster, otp_app: :my_app.
  • :namespace - stable data namespace within the KV cluster and object store (default "default"). Named instances require an explicit namespace so renaming an instance does not change its stored layout. Changing the namespace leaves existing files at the old layout. Instances with the same namespace share records, intents, and object registrations.
  • :timeout - timeout for each KV or object-store call (default no timeout for object-store calls); operations can override it.

:clock is an internal test hook.

Errors

Invalid file data (ref, body, metadata, content type, expected SHA-256, :if_version and :range) returns an error tuple. Invalid control options (:storage, :files, :timeout) and unknown option names raise ArgumentError.

  • {:error, {:conflict, version}} - an if_version: condition does not hold; version is the file's current one, or nil.
  • {:error, :expired} - an upload took longer than upload_timeout_ms; its object is deleted.
  • {:error, {:range_not_satisfiable, size}} - a read's :range contains no byte of the file; size is the file's size.
  • {:error, :checksum_mismatch}, {:error, :too_large_for_inline}, {:error, {:bad_request, reason}}.
  • {:error, :unavailable | :timeout} - from Slap.KV or the store. A write may still have been applied; get/1 shows whether it was.

Telemetry

put/3, delete/2, get/2, read/2, stream/2, and list/2 emit Telemetry spans under [:slap, :files, operation], with :start, :stop, and :exception events. Metadata includes :files, the instance name; a stop event also has :outcome (:ok or :error). A read or stream with a :range has it in its metadata. The stream/2 span measures opening the body stream; consuming it happens after that span ends.

Summary

Types

The bytes a read returns: {first, last} (inclusive, as in HTTP), {first, :eof}, or {:last, n} (the last n bytes).

Functions

Returns a specification to start this module under a supervisor.

Deletes a file. Returns :ok once it is gone (also if there was none, without :if_version); its body is deleted after retention_ms. if_version: makes it conditional, as for put/3.

The file's metadata, or {:ok, nil}.

Lists a partition's files in id order, including their versions. Takes the :prefix, :gte, :lt, :limit and :cursor of Slap.KV.scan/2, and returns {:ok, %{files: files, cursor: cursor}}. An input cursor: nil starts at the beginning of the selected range; a returned cursor: nil means there are no more pages. A binary cursor remains valid after the file it follows is deleted. Supplied :prefix, :gte, and :lt values must be binaries.

A new random id, for a ref.

Writes a file: creates it, or replaces its body and metadata. body is a binary or an enumerable of binaries (read once). Returns the file's new Slap.Files.File once it is durable.

The file's body, or {:ok, nil}. See stream/2 for large bodies, and for the :range and :if_version options.

The file and its body as a lazy stream of binaries, or {:ok, nil}. An object body stays readable for retention_ms after the file is replaced or deleted. A store failure while reading the returned stream raises Slap.SlateDB.Error; failure to open the download returns {:error, :unavailable | :timeout}.

Types

error()

@type error() ::
  Slap.KV.error()
  | :checksum_mismatch
  | :expired
  | :too_large_for_inline
  | {:range_not_satisfiable, non_neg_integer()}

range()

@type range() ::
  {non_neg_integer(), non_neg_integer() | :eof} | {:last, non_neg_integer()}

The bytes a read returns: {first, last} (inclusive, as in HTTP), {first, :eof}, or {:last, n} (the last n bytes).

ref()

@type ref() :: {partition :: binary(), id :: binary()}

Functions

child_spec(init_arg)

@spec child_spec(keyword()) :: Supervisor.child_spec()

Returns a specification to start this module under a supervisor.

See Supervisor.

delete(ref, opts \\ [])

@spec delete(ref(), keyword()) :: :ok | {:error, error()}

Deletes a file. Returns :ok once it is gone (also if there was none, without :if_version); its body is deleted after retention_ms. if_version: makes it conditional, as for put/3.

get(ref, opts \\ [])

@spec get(ref(), keyword()) :: {:ok, Slap.Files.File.t() | nil} | {:error, error()}

The file's metadata, or {:ok, nil}.

list(partition, opts \\ [])

@spec list(binary(), keyword()) ::
  {:ok, %{files: [Slap.Files.File.t()], cursor: binary() | nil}}
  | {:error, error()}

Lists a partition's files in id order, including their versions. Takes the :prefix, :gte, :lt, :limit and :cursor of Slap.KV.scan/2, and returns {:ok, %{files: files, cursor: cursor}}. An input cursor: nil starts at the beginning of the selected range; a returned cursor: nil means there are no more pages. A binary cursor remains valid after the file it follows is deleted. Supplied :prefix, :gte, and :lt values must be binaries.

new_id()

@spec new_id() :: binary()

A new random id, for a ref.

put(ref, body, opts \\ [])

@spec put(ref(), binary() | Enumerable.t(), keyword()) ::
  {:ok, Slap.Files.File.t()} | {:error, error()}

Writes a file: creates it, or replaces its body and metadata. body is a binary or an enumerable of binaries (read once). Returns the file's new Slap.Files.File once it is durable.

Options:

  • :if_version - :absent to create only, or the version the file must be at.
  • :storage - :auto (default), :inline or :object.
  • :content_type - default "application/octet-stream".
  • :metadata - a map of strings to strings (default %{}).
  • :expected_sha256 - the body's expected SHA-256 (32 bytes).
  • :files - the instance name (default Slap.Files).
  • :timeout - overrides the instance's timeout for each KV or object-store call.

Writing the body, content type, metadata and storage the file already has without a condition returns the file as it is. With :if_version, the condition must hold even when the content is unchanged.

read(ref, opts \\ [])

@spec read(ref(), keyword()) :: {:ok, binary() | nil} | {:error, error()}

The file's body, or {:ok, nil}. See stream/2 for large bodies, and for the :range and :if_version options.

start_link(opts)

@spec start_link(keyword()) :: Supervisor.on_start()

stream(ref, opts \\ [])

@spec stream(ref(), keyword()) ::
  {:ok,
   {Slap.Files.File.t(), Enumerable.t()}
   | {Slap.Files.File.t(), {non_neg_integer(), non_neg_integer()},
      Enumerable.t()}
   | nil}
  | {:error, error()}

The file and its body as a lazy stream of binaries, or {:ok, nil}. An object body stays readable for retention_ms after the file is replaced or deleted. A store failure while reading the returned stream raises Slap.SlateDB.Error; failure to open the download returns {:error, :unavailable | :timeout}.

Options:

  • :range - read only part of the body (see range/0). Returns {:ok, {file, {first, last}, chunks}}, where {first, last} is the range clamped to the file's size, as HTTP clamps it: a last past the end becomes the last byte. A range that starts past the end, {:last, 0}, and every range of an empty file return {:error, {:range_not_satisfiable, size}}. For an object body, only the requested bytes are downloaded.
  • :if_version - the version the file must have; otherwise the read returns {:error, {:conflict, version}}. To read a file in several ranges from the same body, pass the version from the first read to the rest.
  • :files, :timeout - as for put/3.