Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion nodejs/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,19 @@
"path": "https://github.com/fission/examples/blob/main/nodejs/echo.js",
"tag": ["node.js","Websocket"],
"language": "Javascript"
},
{
"name": "Stateful Counter",
"description": "A per-user counter using Fission's built-in function state (durable key/value with compare-and-swap), no external Redis or database",
"path": "https://github.com/fission/examples/blob/main/nodejs/stateful-counter.js",
"tag": ["node.js","State","Stateful"],
"language": "Javascript"
},
{
"name": "Stateful Session Store",
"description": "A self-expiring login-session store using Fission's built-in function state with a TTL, no external Redis or database",
"path": "https://github.com/fission/examples/blob/main/nodejs/stateful-session.js",
"tag": ["node.js","State","Stateful","Session"],
"language": "Javascript"
}
]
]
59 changes: 59 additions & 0 deletions nodejs/stateful-counter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// A per-user counter backed by Fission's built-in function state — no external
// Redis or database, and no credentials in this code.
//
// Deploy (the --state flag opts the function into its own keyspace; requires an
// install with `statestore.enabled=true` and `functionState.enabled=true`):
//
// fission function create --name counter --env nodejs --code stateful-counter.js --state
// fission route create --name counter-rt --function counter --url /counter --method GET
// curl "$FISSION_ROUTER/counter?user=alice" # -> 1, 2, 3, ... per user
//
// Fission injects FISSION_STATE_URL (the state API) and FISSION_STATE_TOKEN_PATH
// (a JSON file with this function's scoped {namespace, keyspace, token}). We talk
// to the API over plain HTTP — no SDK to install.

const fs = require('fs');

function stateClient() {
const c = JSON.parse(fs.readFileSync(process.env.FISSION_STATE_TOKEN_PATH, 'utf8'));
const base = process.env.FISSION_STATE_URL;
const headers = {
'Authorization': 'Bearer ' + c.token,
'X-Fission-State-Namespace': c.namespace,
'X-Fission-State-Keyspace': c.keyspace,
};
return {
// Returns { value, version } or null when the key is absent.
async get(key) {
const r = await fetch(`${base}/v1/state/${key}`, { headers });
if (r.status === 404) return null;
return { value: await r.text(), version: Number(r.headers.get('x-fission-state-version')) };
},
// Compare-and-swap: the write lands only if the key is still at ifVersion
// (use 0 for "create only"). Returns the HTTP status: 204 ok, 412 conflict.
async set(key, value, ifVersion) {
const h = { ...headers, 'If-Match': String(ifVersion) };
const r = await fetch(`${base}/v1/state/${key}`, { method: 'PUT', headers: h, body: value });
return r.status;
},
};
}

module.exports = async function (context) {
const state = stateClient();
const user = (context.request.query && context.request.query.user) || 'anon';

// get -> increment -> compare-and-swap. If another request for the same user
// incremented in between (412), read the fresh value and retry. No locks, no
// lost increments.
for (let attempt = 0; attempt < 10; attempt++) {
const cur = await state.get(user);
const next = (cur ? Number(cur.value) : 0) + 1;
const status = await state.set(user, String(next), cur ? cur.version : 0);
if (status === 204) {
return { status: 200, body: `${user}: ${next}\n` };
}
// 412 -> someone else won the race; loop and try again.
}
return { status: 500, body: 'too much contention\n' };
};
64 changes: 64 additions & 0 deletions nodejs/stateful-session.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// A login-session store backed by Fission's built-in function state. Sessions
// self-expire via a TTL, so you never write a cleanup job — no external Redis
// or database, no credentials in this code.
//
// Deploy with a default TTL so idle sessions clean themselves up after 30 min:
//
// fission function create --name sessions --env nodejs --code stateful-session.js \
// --state --state-keyspace user-sessions --state-ttl 30m
// fission route create --name sessions-rt --function sessions --url /session --method GET
//
// # start a session: curl "$FISSION_ROUTER/session?login=alice" -> a session id
// # check a session: curl "$FISSION_ROUTER/session?sid=<id>" -> the user, or 401
//
// Requires an install with `statestore.enabled=true` and
// `functionState.enabled=true`. Fission injects FISSION_STATE_URL and
// FISSION_STATE_TOKEN_PATH into the pod.

const fs = require('fs');
const crypto = require('crypto');

function stateClient() {
const c = JSON.parse(fs.readFileSync(process.env.FISSION_STATE_TOKEN_PATH, 'utf8'));
const base = process.env.FISSION_STATE_URL;
const headers = {
'Authorization': 'Bearer ' + c.token,
'X-Fission-State-Namespace': c.namespace,
'X-Fission-State-Keyspace': c.keyspace,
};
return {
async get(key) {
const r = await fetch(`${base}/v1/state/${key}`, { headers });
return r.status === 404 ? null : await r.text();
},
// The keyspace default TTL (--state-ttl) applies; pass an explicit ttl
// (a Go duration like "10m") to override it for one key.
async set(key, value, ttl) {
const h = { ...headers };
if (ttl) h['X-Fission-State-TTL'] = ttl;
await fetch(`${base}/v1/state/${key}`, { method: 'PUT', headers: h, body: value });
},
};
}

module.exports = async function (context) {
const state = stateClient();
const q = context.request.query || {};

// ?login=<user> -> mint a session id, store the session document (expires per
// the function's --state-ttl), return the id.
if (q.login) {
const sid = crypto.randomUUID();
await state.set(sid, JSON.stringify({ user: q.login, since: Date.now() }));
return { status: 200, body: sid + '\n' };
}

// ?sid=<id> -> look the session up; a missing/expired session is a 401.
if (q.sid) {
const doc = await state.get(q.sid);
if (!doc) return { status: 401, body: 'session expired or invalid\n' };
return { status: 200, body: doc + '\n' };
}

return { status: 400, body: 'pass ?login=<user> to start a session or ?sid=<id> to check one\n' };
};
9 changes: 8 additions & 1 deletion python/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,12 @@
"path": "https://github.com/fission/examples/tree/main/python/websocket",
"tag": ["Python","Application","Websocket"],
"language": "Python"
},
{
"name": "Stateful Counter",
"description": "A per-user counter using Fission's built-in function state (durable key/value with compare-and-swap), no external Redis or database",
"path": "https://github.com/fission/examples/blob/main/python/stateful_counter.py",
"tag": ["Python","State","Stateful"],
"language": "Python"
}
]
]
75 changes: 75 additions & 0 deletions python/stateful_counter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# A per-user counter backed by Fission's built-in function state — no external
# Redis or database, and no credentials in this code.
#
# Deploy (the --state flag opts the function into its own keyspace; requires an
# install with `statestore.enabled=true` and `functionState.enabled=true`):
#
# fission function create --name counter-py --env python --code stateful_counter.py --state
# fission route create --name counter-py-rt --function counter-py --url /counter-py --method GET
# curl "$FISSION_ROUTER/counter-py?user=alice" # -> 1, 2, 3, ... per user
#
# Fission injects FISSION_STATE_URL (the state API) and FISSION_STATE_TOKEN_PATH
# (a JSON file with this function's scoped {namespace, keyspace, token}). We talk
# to the API over plain HTTP from the standard library — no SDK to install.

import json
import os
import urllib.error
import urllib.request

from flask import request


def _client():
with open(os.environ["FISSION_STATE_TOKEN_PATH"]) as f:
creds = json.load(f)
base = os.environ["FISSION_STATE_URL"]
headers = {
"Authorization": "Bearer " + creds["token"],
"X-Fission-State-Namespace": creds["namespace"],
"X-Fission-State-Keyspace": creds["keyspace"],
}

def _req(method, path, body=None, extra=None):
h = dict(headers, **(extra or {}))
req = urllib.request.Request(base + path, data=body, method=method, headers=h)
try:
resp = urllib.request.urlopen(req)
return resp.status, resp.read(), resp.headers
except urllib.error.HTTPError as e:
return e.code, e.read(), e.headers

class Client:
# Returns (value, version) or (None, 0) when the key is absent.
def get(self, key):
status, body, hdr = _req("GET", f"/v1/state/{key}")
if status == 404:
return None, 0
return body.decode(), int(hdr.get("X-Fission-State-Version", 0))

# Compare-and-swap: the write lands only if the key is still at
# if_version (use 0 for "create only"). Returns the HTTP status:
# 204 ok, 412 conflict.
def set(self, key, value, if_version):
status, _, _ = _req(
"PUT", f"/v1/state/{key}", value.encode(), {"If-Match": str(if_version)}
)
return status

return Client()


def main():
state = _client()
user = request.args.get("user", "anon")

# get -> increment -> compare-and-swap. If another request for the same user
# incremented in between (412), read the fresh value and retry. No locks, no
# lost increments.
for _ in range(10):
value, version = state.get(user)
nxt = (int(value) if value is not None else 0) + 1
if state.set(user, str(nxt), version) == 204:
return f"{user}: {nxt}\n"
# 412 -> someone else won the race; loop and try again.
return "too much contention\n", 500