From a6ea431f872b11850718f3c236202692f92b4c55 Mon Sep 17 00:00:00 2001 From: Isaac Harris-Holt Date: Tue, 3 Sep 2024 18:29:50 +0100 Subject: [PATCH] add expiration to cache --- src/pevensie/cache.gleam | 6 ++-- src/pevensie/drivers.gleam | 5 +-- src/pevensie/drivers/postgres.gleam | 49 ++++++++++++++++++++++------- v1.sql | 1 + 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/pevensie/cache.gleam b/src/pevensie/cache.gleam index 4a695ab..235b87c 100644 --- a/src/pevensie/cache.gleam +++ b/src/pevensie/cache.gleam @@ -1,3 +1,4 @@ +import gleam/option.{type Option} import pevensie/drivers.{ type CacheDriver, type Connected, type Disabled, type Disconnected, } @@ -25,10 +26,11 @@ pub fn store( resource_type: String, key: String, value: String, + ttl_seconds: Option(Int), ) -> Result(Nil, Nil) { let assert cache.CacheConfig(driver) = pevensie.cache_config - driver.store(driver.driver, resource_type, key, value) + driver.store(driver.driver, resource_type, key, value, ttl_seconds) } pub fn get( @@ -41,7 +43,7 @@ pub fn get( ), resource_type: String, key: String, -) -> Result(String, Nil) { +) -> Result(Option(String), Nil) { let assert cache.CacheConfig(driver) = pevensie.cache_config driver.get(driver.driver, resource_type, key) diff --git a/src/pevensie/drivers.gleam b/src/pevensie/drivers.gleam index 8f1adc5..3a386ce 100644 --- a/src/pevensie/drivers.gleam +++ b/src/pevensie/drivers.gleam @@ -1,5 +1,6 @@ import gleam/dynamic.{type Decoder} import gleam/json +import gleam/option.{type Option} import pevensie/internal/user.{type User, type UserInsert} pub type Connected @@ -41,10 +42,10 @@ pub type AuthDriver(driver, user_metadata) { } type CacheStoreFunction(cache_driver) = - fn(cache_driver, String, String, String) -> Result(Nil, Nil) + fn(cache_driver, String, String, String, Option(Int)) -> Result(Nil, Nil) type CacheGetFunction(cache_driver) = - fn(cache_driver, String, String) -> Result(String, Nil) + fn(cache_driver, String, String) -> Result(Option(String), Nil) type CacheDeleteFunction(cache_driver) = fn(cache_driver, String, String) -> Result(Nil, Nil) diff --git a/src/pevensie/drivers/postgres.gleam b/src/pevensie/drivers/postgres.gleam index 7070c7f..ffb1d42 100644 --- a/src/pevensie/drivers/postgres.gleam +++ b/src/pevensie/drivers/postgres.gleam @@ -4,7 +4,8 @@ import gleam/dynamic.{ type DecodeErrors as DynamicDecodeErrors, type Decoder, DecodeError as DynamicDecodeError, } -import gleam/function +import gleam/erlang/process +import gleam/int import gleam/io import gleam/json import gleam/option.{type Option, None, Some} @@ -378,8 +379,8 @@ pub fn new_cache_driver(config: PostgresConfig) -> CacheDriver(Postgres) { driver: Postgres(config |> postgres_config_to_pgo_config, None), connect: connect, disconnect: disconnect, - store: fn(driver, resource_type, key, value) { - store(driver, resource_type, key, value) + store: fn(driver, resource_type, key, value, ttl_seconds) { + store(driver, resource_type, key, value, ttl_seconds) // TODO: Handle errors |> result.map_error(fn(err) { io.debug(err) @@ -410,19 +411,26 @@ fn store( resource_type: String, key: String, value: String, + ttl_seconds: Option(Int), ) -> Result(Nil, PostgresError) { let assert Postgres(_, Some(conn)) = driver - let sql = - " + let expires_at_sql = case ttl_seconds { + None -> "null" + Some(ttl_seconds) -> + "now() + interval '" <> int.to_string(ttl_seconds) <> " seconds'" + } + let sql = " insert into pevensie.\"cache\" ( resource_type, key, - value + value, + expires_at ) values ( $1, $2, - $3 + $3, + " <> expires_at_sql <> " ) on conflict (resource_type, key) do update set value = $3" @@ -446,12 +454,16 @@ fn get( driver: Postgres, resource_type: String, key: String, -) -> Result(String, PostgresError) { +) -> Result(Option(String), PostgresError) { let assert Postgres(_, Some(conn)) = driver let sql = " - select value::text + select + value::text, + -- Returns true only if the exporation time is + -- set and has passed + (expires_at is not null and expires_at < now()) as expired from pevensie.\"cache\" where resource_type = $1 and key = $2" @@ -460,14 +472,27 @@ fn get( sql, conn, [pgo.text(resource_type), pgo.text(key)], - dynamic.decode1(function.identity, dynamic.element(0, dynamic.string)), + dynamic.decode2( + fn(value, expired) { #(value, expired) }, + dynamic.element(0, dynamic.string), + dynamic.element(1, dynamic.bool), + ), ) |> result.map_error(QueryError) use response <- result.try(query_result) case response.rows { - [] -> Error(NotFound) - [value] -> Ok(value) + [] -> Ok(None) + // If no expiration is set, the value is valid forever + [#(value, False)] -> { + Ok(Some(value)) + } + // If the value has expired, return None and delete the key + // in an async task + [#(_, True)] -> { + process.start(fn() { delete(driver, resource_type, key) }, False) + Ok(None) + } _ -> Error(InternalError("Unexpected number of rows returned")) } } diff --git a/v1.sql b/v1.sql index 20f1e69..0320ae8 100644 --- a/v1.sql +++ b/v1.sql @@ -73,5 +73,6 @@ create table if not exists pevensie."cache" ( resource_type text not null, key text not null unique, value text not null, + expires_at timestamptz, primary key (resource_type, key) );