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:
sessions:
id: sid
time: created_at
expires_field: expires_atDeclare the expiry field from code
ndb.yml is enough for every SDK. Each SDK can also declare the field itself.
type Session struct {
SID string `ndb:"sid,id"`
CreatedAt time.Time `ndb:"created_at,time"`
ExpiresAt time.Time `ndb:"expires_at,expires"`
}ndb.ensure_collection("sessions", id: "sid", time: "created_at", expires_field: "expires_at")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 },
},
})ndb PUT /v1/c/sessions '{"id":"sid","time":"created_at","expires_field":"expires_at"}' >/dev/nullInsert 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.
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)")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)"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)')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.
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())puts "ttl s-short: #{ndb.ttl('sessions', 's-short')}s, s-long: #{ndb.ttl('sessions', 's-long')}s"console.log(`ttl s-short: ${await sessions.ttl('s-short')}s, s-long: ${await sessions.ttl('s-long')}s`)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.
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())ndb.expire("sessions", "s-long", in: "30m")
puts "expire s-long 30m -> ttl #{ndb.ttl('sessions', 's-long')}s"await sessions.expire('s-long', '30m')
console.log(`expire s-long 30m -> ttl ${await sessions.ttl('s-long')}s`)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.
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)")
}ndb.persist("sessions", "s-keep")
puts "persist s-keep -> ttl -1 (never expires)" if ndb.ttl("sessions", "s-keep") == -1await sessions.persist('s-keep')
if ((await sessions.ttl('s-keep')) === -1) {
console.log('persist s-keep -> ttl -1 (never expires)')
}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)"
fiAfter 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.
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
}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?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')
}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"
fiOutput
The four programs print the same 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: goneNotes
- Every SDK supports every step:
expire_in,ttl,expire,persistand declaringexpires_fieldfrom code (Go tagexpires, Rubyensure_collection(expires_field:), TypeScriptexpiresin the schema, curlPUT). ttl: 24hwith atimefield is a derived expiry: every document ends a fixed time after itstimefield.expireandpersistfail on it withttl_derived.- Each
expireorpersistis a normal write: new version,watchevent, andif_versionworks. - More: TTL like Redis and Compaction, retention and TTL.
Run it
cd examples
make run-ttl-sessions LANG=go # go, ruby, ts or curl
make run-ttl-sessions # every languageSource: examples/ttl-sessions. The programs read NDB_URL and NDB_TOKEN, and they can run again and again.