IsleDB / Docs / HTTP API reference

HTTP API reference

The writer service takes writes; reader services serve reads, scans, and the change feed. Both speak JSON over HTTP, from any language.

Writer API

The writer service stores keys and values in one IsleDB database. You can write a key, delete a key, write several keys at once, and check whether a write was saved.

To run the service, see Writer service.

Every path starts with /v1/. Every response body is JSON.

Quick start

# Write a value
curl -X PUT -H "Authorization: Bearer $TOKEN" \
  --data '{"name":"a"}' 'http://isledb-writer:8070/v1/kv/user/42'
# {"write_id":"7.10442","revision":"1042"}

# Delete it
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  'http://isledb-writer:8070/v1/kv/user/42'

Authentication

Send your token in every request:

Authorization: Bearer <token>

Without a valid token, the writer answers 401.

Endpoints

Request Does
PUT /v1/kv/{key} Writes one key. The request body is the value.
DELETE /v1/kv/{key} Deletes one key.
POST /v1/kv Writes and deletes several keys at once.
GET /v1/writes/{write_id} Tells whether a write was saved.

Keys and values

Write a key

PUT /v1/kv/user/42
Content-Type: application/json

{"name":"a"}
200 OK

{"write_id":"7.10442","revision":"1042"}

The writer answers once the value is saved, usually within a few hundred milliseconds. A 200 means the value is stored durably.

The response has two fields:

Expiring values. Add ?ttl= to make the value expire after a while:

PUT /v1/kv/session/abc?ttl=24h

Durations look like 500ms, 30s, 10m or 24h. Without ttl, the value never expires. An empty ttl= is an error, so a missing value in your code doesn't silently store a key forever.

Delete a key

DELETE /v1/kv/user/42
200 OK

{"write_id":"7.10443","revision":"1043"}

Deleting a key that doesn't exist is not an error.

Write several keys at once

Send a list of operations. They are applied together: anyone reading the database sees either all of them or none of them.

POST /v1/kv
Content-Type: application/json

{"ops":[
  {"op":"put","key":"order/42","value":"{\"total\":9}","ttl":"24h"},
  {"op":"put","key":"stock/7","value_base64":"MTI="},
  {"op":"delete","key":"cart/42"}
]}
200 OK

{"write_id":"7.10446","revision":"1044"}

Batches are much faster than one request per key when you write a lot of data. A batch can hold up to about 16 MiB of keys and values. If it's too large, the writer answers 413; split it into smaller batches.

Write without waiting

To get an answer as soon as the writer has accepted a write, without waiting for it to be saved, add this header:

Prefer: respond-async
202 Accepted
Location: /v1/writes/7.10447

{"write_id":"7.10447"}

The write is saved shortly afterwards. Until then it can still be lost, for example if the writer crashes. To find out when it's saved, check it at the Location URL.

Waiting less. To wait at most a few seconds for a write to be saved, send Prefer: wait=N, where N is a number of seconds. If the write isn't confirmed in time, the writer answers 504 with its write_id.

Check a write

GET /v1/writes/7.10442
200 OK

{"status":"committed","revision":"1042"}
status Meaning
committed The write is saved.
pending The write is accepted but not saved yet. Ask again after the number of seconds in the Retry-After header.
not_committed The write was not saved and never will be. It's safe to send it again.
unknown The writer can't tell, because the write is too old.

A writer that was replaced, and can't tell what happened to its own write, answers 503 unavailable instead of unknown: the writer now holding the database can answer. Ask again after Retry-After seconds.

Sending a request body

PUT and POST requests must include a Content-Length header. Most HTTP clients add it automatically: curl with --data, Python requests, JavaScript fetch with a string body, and Go with a byte slice. Requests without it are answered 411.

Errors

Errors have a JSON body:

503 Service Unavailable
Retry-After: 1

{"error":"backpressure","message":"too many writes waiting to commit; retry"}
Status error Meaning What to do
400 bad_request The request is malformed: bad JSON, an unknown op, a bad or empty ttl, or an unknown query parameter. The message says what's wrong. Fix the request.
400 invalid_mutation A key, value or TTL isn't allowed: an empty or too-long key, a too-large value, a negative TTL, or an empty batch. The message says which operation. Fix the request.
400 bad_write_id The write ID isn't valid. Check the ID.
401 Missing or wrong token. Send a valid token.
404 not_found No such endpoint. Check the path.
405 method_not_allowed Wrong method for this path. The Allow header lists the right ones. Use an allowed method.
408 body_timeout The request body arrived too slowly. Retry.
411 length_required The request has no Content-Length header. Send Content-Length.
413 too_large The value or batch is too large. Send a smaller value, or split the batch.
503 backpressure The writer is busy. Nothing was written. Retry after Retry-After seconds.
503 unavailable The writer isn't taking writes right now, for example while restarting; nothing was written. On GET /v1/writes/…, this writer was replaced and another can answer. Retry after Retry-After seconds.
503 not_committed The write was accepted but will never be saved. The body includes its write_id. Send it again.
504 outcome_unknown The write was accepted but not confirmed in time. It may still be saved. The body includes its write_id. Check it at GET /v1/writes/{write_id} before sending it again.

Retrying safely

Reader API

The reader service reads keys, and scans ranges of keys, from one IsleDB database. Run as many readers as you like: each serves reads on its own, so add replicas to serve more.

To run the service, see Reader service. Writes go to the writer service, described in the writer HTTP API.

Every path starts with /v1/.

Quick start

curl -H "Authorization: Bearer $TOKEN" 'http://isledb-reader:8080/v1/kv/user/42'
# {"name":"a"}

Authentication

Send your token in every request:

Authorization: Bearer <token>

Without a valid token, the reader answers 401.

Endpoints

Request Does
GET /v1/kv/{key} Reads one key. The response body is the value.
HEAD /v1/kv/{key} The same, without the body: whether the key exists, and its size.
GET /v1/kv?prefix=… Lists keys and their values, a page at a time.
GET /v1/changes?from=… Reads the database's change feed, a page at a time.

The paths match the writer's, so a gateway can serve one API: send GET and HEAD requests for /v1/kv, /v1/kv/… and /v1/changes to readers, and everything else, including GET /v1/writes/…, to the writer.

Read a key

GET /v1/kv/user/42
200 OK
Content-Type: application/octet-stream
Content-Length: 12
Isledb-Revision: 1042

{"name":"a"}

Scan keys

GET /v1/kv?prefix=user/&limit=2
200 OK
Content-Type: application/json
Isledb-Revision: 1042

{"items":[{"key":"user/1","value":"{\"name\":\"a\"}"},
          {"key":"user/2","value":"{\"name\":\"b\"}"}],
 "next_page":"AXVzZXIvMw"}

Choose the keys with prefix, or with start and end; leave all three out to scan every key:

Parameter Meaning
prefix Keys that start with it. Not with start or end.
start The first key, included.
end Where to stop: keys before it, not itself.
limit The most items in a page: 1 to 1000, 100 by default.
page The next_page from the page before.

For example, with the keys order/0001 to order/0500:

GET /v1/kv?start=order/0100&end=order/0200     order/0100 to order/0199
GET /v1/kv?start=order/0450                    order/0450 and every key after it
GET /v1/kv?end=order/0003                      every key before order/0003
GET /v1/kv?prefix=order/                       every key starting with order/

end itself is never included. To include it, add a zero byte: end=order/0200%00 stops right after order/0200.

Follow the change feed

When the database has a change feed (the writer's change_feed.payload), readers serve it:

GET /v1/changes?from=oldest&limit=2
200 OK
Content-Type: application/json

{"changes":[{"sequence":41,"revision":1042,"op":"put","key":"user/42","value":"{\"name\":\"a\"}"},
            {"sequence":42,"revision":1043,"op":"delete","key":"user/7"}],
 "next":"cf1_AAAAAAAAAAIAAAAAAAAAAA",
 "caught_up":true}
Parameter Meaning
from Where to read from: a next from an earlier page, oldest for every change still kept, or head for only changes from now on. Required.
limit The most changes in a page: 1 to 10000, 1000 by default.

When your cursor is too old. If the database's change-feed retention has deleted changes after your cursor, the answer is 410 with {"error":"cursor_expired","oldest":"<cursor>",…}. Either continue from oldest, accepting the gap, or rebuild your copy of the data (below).

Start from the data, then follow the feed. To build a copy of the database and keep it current:

  1. Read GET /v1/changes?from=head and save its next.
  2. Copy every key with a scan.
  3. Follow the feed from the saved cursor, applying each put as "set this key" and each delete as "remove this key".

Changes made during the scan are applied again, but the feed holds every later change too, so the copy ends up right.

How fresh a read is

A reader answers from the newest data it has loaded. It loads the latest data about once a second, or, when it reads through an isledb cache, usually within a second of each write. So a read can miss a write made in the last second or so. On Google Cloud Storage, which takes at most one write a second to the database, allow about two seconds.

If a reader can't reach storage, it keeps answering from the data it has until that data is max_pinned_view_age old (1 hour by default), and only then answers 503. So during a storage outage, reads can be up to that old.

Responses carry Cache-Control: no-cache, so caches in front of the readers check before reusing an answer.

To check whether a read includes a particular write, compare its Isledb-Revision with the revision the writer returned: a read at or past that revision includes it.

Errors

Error responses are JSON:

{"error":"key_not_found","message":"no such key"}
Status error Meaning What to do
400 bad_request The request is malformed: an empty key, a key not encoded correctly, a query parameter on a key read, a scan parameter that is unknown, repeated, empty or out of range, a page from another scan, or a change-feed from or limit that isn't valid. Fix the request.
401 The token is missing or wrong. The body is plain text. Send a valid token.
404 key_not_found The key doesn't exist, or its TTL has passed.
404 not_found There is no such endpoint. Check the path.
404 change_feed_off The database has no change feed. Turn it on in the writer's change_feed.payload.
410 cursor_expired Changes after your cursor are no longer kept. The body's oldest is where the feed starts now. See When your cursor is too old.
405 method_not_allowed A write was sent to a reader, or a scan or change-feed read wasn't a GET. Send writes to the writer.
503 backpressure The reader is busy. Retry after Retry-After seconds, or on another replica.
503 unavailable The reader has no current data to read from, for example while starting, or after losing access to storage for longer than the database's max_pinned_view_age (1 hour by default); or reading storage failed, or took longer than a minute. Retry after Retry-After seconds, or on another replica.