Install

Expiring sessions ​

A document can expire by itself, like a key in Redis. You set the time with expire_in, read it with ttl, move it with expire, and keep it forever with persist. After the time, no read returns it.

When to use ​

Sessions, tokens, caches and locks: data that must disappear by itself.

Collection ​

Declared in examples/ndb.yml. expires_field names the field where the daemon stores the end time:

yaml
sessions:
  id: sid
  time: created_at
  expires_field: expires_at

Declare the expiry field from code ​

ndb.yml is enough for every SDK. Each SDK can also declare the field itself.

go
type Session struct {
	SID       string    `ndb:"sid,id"`
	CreatedAt time.Time `ndb:"created_at,time"`
	ExpiresAt time.Time `ndb:"expires_at,expires"`
}
rb
ndb.ensure_collection("sessions", id: "sid", time: "created_at", expires_field: "expires_at")
ts
const sessions = ndb.model('sessions', {
  id: 'sid',
  time: 'created_at',
  expires: 'expires_at',
  fields: {
    sid: { type: 'string' },
    created_at: { type: 'date' },
    expires_at: { type: 'date', optional: true },
  },
})
sh
ndb PUT /v1/c/sessions '{"id":"sid","time":"created_at","expires_field":"expires_at"}' >/dev/null

Insert with expire_in ​

expire_in is a number of seconds or a text such as "30m" or "1h". It applies to every document of the call, so the short session has its own call.

go
now := time.Now().UTC()

_, err = sessions.InsertWith(ctx, []Session{{SID: "s-short", CreatedAt: now}}, ndb.ExpireIn(2*time.Second))
if err != nil {
	return err
}

_, err = sessions.InsertWith(ctx, []Session{{SID: "s-long", CreatedAt: now}, {SID: "s-keep", CreatedAt: now}}, ndb.ExpireIn(time.Hour))
if err != nil {
	return err
}

fmt.Println("insert s-short (expire_in 2s), s-long and s-keep (expire_in 1h)")
rb
now = Time.now.utc.iso8601(3)

ndb.insert("sessions", [{ sid: "s-short", created_at: now }], expire_in: 2)
ndb.insert("sessions", [{ sid: "s-long", created_at: now }, { sid: "s-keep", created_at: now }], expire_in: "1h")

puts "insert s-short (expire_in 2s), s-long and s-keep (expire_in 1h)"
ts
const now = new Date()

await sessions.insert({ sid: 's-short', created_at: now }, { expireIn: 2 })
await sessions.insert([{ sid: 's-long', created_at: now }, { sid: 's-keep', created_at: now }], { expireIn: '1h' })

console.log('insert s-short (expire_in 2s), s-long and s-keep (expire_in 1h)')
sh
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)

ndb POST /v1/c/sessions/insert "{\"insert\":[{\"sid\":\"s-short\",\"created_at\":\"$now\"}],\"expire_in\":2}" >/dev/null
ndb POST /v1/c/sessions/insert "{\"insert\":[{\"sid\":\"s-long\",\"created_at\":\"$now\"},{\"sid\":\"s-keep\",\"created_at\":\"$now\"}],\"expire_in\":\"1h\"}" >/dev/null

echo "insert s-short (expire_in 2s), s-long and s-keep (expire_in 1h)"

Read the time left ​

ttl returns the seconds left (rounded up), -1 when the document never expires, and a not-found answer when it is missing or expired.

go
short, _, err := sessions.TTL(ctx, "s-short")
if err != nil {
	return err
}

long, _, err := sessions.TTL(ctx, "s-long")
if err != nil {
	return err
}

fmt.Printf("ttl s-short: %.0fs, s-long: %.0fs\n", short.Seconds(), long.Seconds())
rb
puts "ttl s-short: #{ndb.ttl('sessions', 's-short')}s, s-long: #{ndb.ttl('sessions', 's-long')}s"
ts
console.log(`ttl s-short: ${await sessions.ttl('s-short')}s, s-long: ${await sessions.ttl('s-long')}s`)
sh
echo "ttl s-short: $(ttl s-short)s, s-long: $(ttl s-long)s"

Extend one session ​

expire sets a new end time, like EXPIRE in Redis.

go
if _, err := sessions.Expire(ctx, "s-long", 30*time.Minute); err != nil {
	return err
}

left, _, err := sessions.TTL(ctx, "s-long")
if err != nil {
	return err
}

fmt.Printf("expire s-long 30m -> ttl %.0fs\n", left.Seconds())
rb
ndb.expire("sessions", "s-long", in: "30m")

puts "expire s-long 30m -> ttl #{ndb.ttl('sessions', 's-long')}s"
ts
await sessions.expire('s-long', '30m')

console.log(`expire s-long 30m -> ttl ${await sessions.ttl('s-long')}s`)
sh
ndb POST /v1/c/sessions/expire '{"id":"s-long","in":"30m"}' >/dev/null

echo "expire s-long 30m -> ttl $(ttl s-long)s"

Keep one session forever ​

persist removes the end time, like PERSIST in Redis.

go
if _, err := sessions.Persist(ctx, "s-keep"); err != nil {
	return err
}

_, expires, err := sessions.TTL(ctx, "s-keep")
if err != nil {
	return err
}

if !expires {
	fmt.Println("persist s-keep -> ttl -1 (never expires)")
}
rb
ndb.persist("sessions", "s-keep")

puts "persist s-keep -> ttl -1 (never expires)" if ndb.ttl("sessions", "s-keep") == -1
ts
await sessions.persist('s-keep')

if ((await sessions.ttl('s-keep')) === -1) {
  console.log('persist s-keep -> ttl -1 (never expires)')
}
sh
ndb POST /v1/c/sessions/persist '{"id":"s-keep"}' >/dev/null

if [[ "$(ttl s-keep)" == "-1" ]]; then
	echo "persist s-keep -> ttl -1 (never expires)"
fi

After the time passes ​

The expired document is hidden from every read at once. A sweeper removes it from the WAL later, and watch shows that delete with "_expired": true.

go
alive, err := sessions.Where().All(ctx)
if err != nil {
	return err
}

ids := make([]string, len(alive))

for i, s := range alive {
	ids[i] = s.SID
}

fmt.Printf("live sessions: %d (%s)\n", len(alive), strings.Join(ids, ", "))

_, _, err = sessions.TTL(ctx, "s-short")

if errors.Is(err, ndb.ErrNotFound) {
	fmt.Println("ttl s-short: gone")
} else if err != nil {
	return err
}
rb
alive = ndb.find("sessions", {}).rows

puts "live sessions: #{alive.size} (#{alive.map { |s| s['sid'] }.join(', ')})"

puts "ttl s-short: gone" if ndb.ttl("sessions", "s-short").nil?
ts
const alive = await sessions.all()

console.log(`live sessions: ${alive.length} (${alive.map((s) => s.sid).join(', ')})`)

if ((await sessions.ttl('s-short')) === null) {
  console.log('ttl s-short: gone')
}
sh
alive=$(ndb POST /v1/c/sessions/find '{}' | jq -r '.sid' | paste -sd, - | sed 's/,/, /g')

echo "live sessions: $(ndb_header X-Ndb-Total POST /v1/c/sessions/find '{"count":true,"limit":1}') ($alive)"

if [[ "$(ndb_status GET /v1/c/sessions/ttl/s-short)" == "404" ]]; then
	echo "ttl s-short: gone"
fi

Output ​

The four programs print the same text.

text
insert s-short (expire_in 2s), s-long and s-keep (expire_in 1h)
ttl s-short: 2s, s-long: 3600s
expire s-long 30m -> ttl 1800s
persist s-keep -> ttl -1 (never expires)
waiting 3s...
live sessions: 2 (s-long, s-keep)
ttl s-short: gone

Notes ​

  • Every SDK supports every step: expire_in, ttl, expire, persist and declaring expires_field from code (Go tag expires, Ruby ensure_collection(expires_field:), TypeScript expires in the schema, curl PUT).
  • ttl: 24h with a time field is a derived expiry: every document ends a fixed time after its time field. expire and persist fail on it with ttl_derived.
  • Each expire or persist is a normal write: new version, watch event, and if_version works.
  • More: TTL like Redis and Compaction, retention and TTL.

Run it ​

sh
cd examples
make run-ttl-sessions LANG=go   # go, ruby, ts or curl
make run-ttl-sessions           # every language

Source: examples/ttl-sessions. The programs read NDB_URL and NDB_TOKEN, and they can run again and again.