diff --git a/DOCS.md b/DOCS.md index 12d8c82cc10a9db727700560dacf7ad91e7ef64b..bf4cc43818cbccd7b80889bab3863d5549439732 100644 --- a/DOCS.md +++ b/DOCS.md @@ -506,7 +506,7 @@ ## stash In-memory key-value store with TTL support, powered by SQLite. -- `stash.use(id, [cleanup_interval])`: Switches to database `id` (0-15). `cleanup_interval` (default 60s) sets how often expired keys are purged. +- `stash.cleanup(interval)`: Sets how often (in seconds) expired keys are purged (default 60s). - `stash.set(key, value, [ttl])`: Stores a value. `ttl` is in seconds. If `0` or omitted, the key never expires. - `stash.get(key)`: Retrieves a value. Returns `nil` if the key is missing or expired. - `stash.del(key)`: Deletes a key. @@ -516,12 +516,11 @@ - `stash.decr(key, [amount])`: Decrements a numeric value. Defaults to 1. - `stash.keys([pattern])`: Returns an array of keys matching the SQL LIKE pattern (default `"%"`). - `stash.ttl(key)`: Returns remaining seconds until expiry. `-1` means no expiry, `-2` means key not found. - `stash.expire(key, ttl)`: Updates the TTL of an existing key. Returns `true` if successful. -- `stash.clear()`: Removes all keys from the current database. +- `stash.clear()`: Removes all keys. - `stash.count()`: Returns the number of non-expired keys. ### Example: Rate Limiting ```lua -stash.use(0) local key = "limit:" .. "127.0.0.1" -- Example IP local count = stash.incr(key, 1) @@ -536,8 +535,8 @@ ``` ### Example: Caching ```lua --- Use DB 0, cleanup every 30 seconds -stash.use(0, 30) +-- Set cleanup every 30 seconds +stash.cleanup(30) -- Store a table with 5 second TTL stash.set("user:session", { id = 42, active = true }, 5) diff --git a/examples/stash_test.lua b/examples/stash_test.lua index 366e04f8b8d5097d763b2e13eacab8a974bc9663..dc4bfa1c7dbbbd97b4e9fbe4a1148d50c249bb08 100644 --- a/examples/stash_test.lua +++ b/examples/stash_test.lua @@ -1,7 +1,7 @@ log.use_colors(true) -log.info("--- Testing stash.use ---") -stash.use(0, 1) -- Use DB 0, cleanup every 1s +log.info("--- Testing stash.cleanup ---") +stash.cleanup(1) -- cleanup every 1s log.info("--- Testing stash.set and stash.get ---") stash.set("name", "Luna", 0) @@ -66,15 +66,6 @@ log.info("--- Testing stash.count/clear ---") stash.clear() assert.equal(0, stash.count()) log.info("Count/Clear passed") - - log.info("--- Testing Multi-DB ---") - stash.use(1, 60) - stash.set("db_key", "db1", 0) - stash.use(0, 60) - assert.equal(nil, stash.get("db_key")) - stash.use(1, 60) - assert.equal("db1", stash.get("db_key")) - log.info("Multi-DB passed") log.info("All stash module tests passed!") os.exit(0) diff --git a/stash.c b/stash.c index 145526410ecd26c80db32d0f3bb563e40f5abfc3..e6d8f5f85dd82ba6d953be6a1ee6a6fa7d182b65 100644 --- a/stash.c +++ b/stash.c @@ -2,87 +2,87 @@ #include "luna.h" #include #include -#define MAX_MEM_DBS 16 - typedef struct { sqlite3 *db; ev_timer cleanup_watcher; int interval; - int id; } MemDB; -static MemDB mem_dbs[MAX_MEM_DBS]; -static int current_db_idx = 0; +static MemDB mdb = {0}; static void cleanup_cb(struct ev_loop *loop, ev_timer *w, int revents) { - MemDB *mdb = (MemDB *)w->data; - if (!mdb->db) { + if (!mdb.db) { return; } sqlite3_stmt *stmt; const char *sql = "DELETE FROM kv WHERE e > 0 AND e < ?;"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) == SQLITE_OK) { + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) == SQLITE_OK) { sqlite3_bind_int64(stmt, 1, (sqlite3_int64)time(NULL)); sqlite3_step(stmt); sqlite3_finalize(stmt); } } -static int l_stash_use(lua_State *L) { - int id = (int)luaL_checkinteger(L, 1); - int interval = (int)luaL_optinteger(L, 2, 60); +static int check_init(lua_State *L) { + if (mdb.db) { + return 0; + } - if (id < 0 || id >= MAX_MEM_DBS) { - return luaL_error(L, "invalid database id (0-%d)", MAX_MEM_DBS - 1); + if (sqlite3_open(":memory:", &mdb.db) != SQLITE_OK) { + return luaL_error(L, "failed to open in-memory database: %s", sqlite3_errmsg(mdb.db)); } - current_db_idx = id; - MemDB *mdb = &mem_dbs[id]; + const char *schema = "CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT, e INTEGER);" + "CREATE INDEX IF NOT EXISTS idx_expiry ON kv(e);"; + if (sqlite3_exec(mdb.db, schema, NULL, NULL, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to create schema: %s", sqlite3_errmsg(mdb.db)); + } - if (!mdb->db) { - if (sqlite3_open(":memory:", &mdb->db) != SQLITE_OK) { - return luaL_error(L, "failed to open in-memory database: %s", sqlite3_errmsg(mdb->db)); - } + if (mdb.interval <= 0) { + mdb.interval = 60; + } - const char *schema = "CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT, e INTEGER);" - "CREATE INDEX IF NOT EXISTS idx_expiry ON kv(e);"; - if (sqlite3_exec(mdb->db, schema, NULL, NULL, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to create schema: %s", sqlite3_errmsg(mdb->db)); - } + mdb.cleanup_watcher.data = &mdb; + ev_timer_init(&mdb.cleanup_watcher, cleanup_cb, mdb.interval, mdb.interval); + ev_timer_start(EV_DEFAULT, &mdb.cleanup_watcher); - mdb->id = id; - mdb->cleanup_watcher.data = mdb; - ev_timer_init(&mdb->cleanup_watcher, cleanup_cb, interval, interval); - ev_timer_start(EV_DEFAULT, &mdb->cleanup_watcher); - } else if (interval != mdb->interval) { - ev_timer_stop(EV_DEFAULT, &mdb->cleanup_watcher); - ev_timer_set(&mdb->cleanup_watcher, interval, interval); - ev_timer_start(EV_DEFAULT, &mdb->cleanup_watcher); + return 0; +} + +static int l_stash_cleanup(lua_State *L) { + int interval = (int)luaL_checkinteger(L, 1); + if (interval <= 0) { + return luaL_error(L, "interval must be greater than 0"); } - mdb->interval = interval; + mdb.interval = interval; + + if (mdb.db) { + ev_timer_stop(EV_DEFAULT, &mdb.cleanup_watcher); + ev_timer_set(&mdb.cleanup_watcher, interval, interval); + ev_timer_start(EV_DEFAULT, &mdb.cleanup_watcher); + } else { + check_init(L); + } + return 0; } static int l_stash_set(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); int ttl = (int)luaL_optinteger(L, 3, 0); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized, call stash.use(id) first"); - } - cJSON *json = lua_to_cjson(L, 2); char *value_str = cJSON_PrintUnformatted(json); cJSON_Delete(json); sqlite3_stmt *stmt; const char *sql = "INSERT OR REPLACE INTO kv (k, v, e) VALUES (?, ?, ?);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { free(value_str); - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); @@ -97,7 +97,7 @@ if (sqlite3_step(stmt) != SQLITE_DONE) { sqlite3_finalize(stmt); free(value_str); - return luaL_error(L, "failed to execute statement: %s", sqlite3_errmsg(mdb->db)); + return luaL_error(L, "failed to execute statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_finalize(stmt); @@ -106,17 +106,13 @@ return 0; } static int l_stash_get(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); - - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized, call stash.use(id) first"); - } sqlite3_stmt *stmt; const char *sql = "SELECT v FROM kv WHERE k = ? AND (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); @@ -140,24 +136,20 @@ return 1; } static int l_stash_del(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized, call stash.use(id) first"); - } - sqlite3_stmt *stmt; const char *sql = "DELETE FROM kv WHERE k = ?;"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); if (sqlite3_step(stmt) != SQLITE_DONE) { sqlite3_finalize(stmt); - return luaL_error(L, "failed to execute statement: %s", sqlite3_errmsg(mdb->db)); + return luaL_error(L, "failed to execute statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_finalize(stmt); @@ -165,16 +157,13 @@ return 0; } static int l_stash_exists(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } sqlite3_stmt *stmt; const char *sql = "SELECT 1 FROM kv WHERE k = ? AND (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)time(NULL)); @@ -186,17 +175,14 @@ return 1; } static int l_stash_incr(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); double amount = luaL_optnumber(L, 2, 1); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } sqlite3_stmt *stmt; const char *get_sql = "SELECT v, e FROM kv WHERE k = ?;"; - if (sqlite3_prepare_v2(mdb->db, get_sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, get_sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); @@ -216,8 +202,8 @@ char val_buf[64]; snprintf(val_buf, sizeof(val_buf), "%g", new_val); const char *set_sql = "INSERT OR REPLACE INTO kv (k, v, e) VALUES (?, ?, ?);"; - if (sqlite3_prepare_v2(mdb->db, set_sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, set_sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, val_buf, -1, SQLITE_TRANSIENT); @@ -237,16 +223,13 @@ return l_stash_incr(L); } static int l_stash_keys(lua_State *L) { + check_init(L); const char *pattern = luaL_optstring(L, 1, "%"); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } sqlite3_stmt *stmt; const char *sql = "SELECT k FROM kv WHERE k LIKE ? AND (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, pattern, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)time(NULL)); @@ -262,16 +245,13 @@ return 1; } static int l_stash_ttl(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } sqlite3_stmt *stmt; const char *sql = "SELECT e FROM kv WHERE k = ? AND (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 2, (sqlite3_int64)time(NULL)); @@ -291,17 +271,14 @@ return 1; } static int l_stash_expire(lua_State *L) { + check_init(L); const char *key = luaL_checkstring(L, 1); int ttl = (int)luaL_checkinteger(L, 2); - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } sqlite3_stmt *stmt; const char *sql = "UPDATE kv SET e = ? WHERE k = ? AND (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_int64 expiry = (ttl > 0) ? (time(NULL) + ttl) : 0; @@ -310,7 +287,7 @@ sqlite3_bind_text(stmt, 2, key, -1, SQLITE_TRANSIENT); sqlite3_bind_int64(stmt, 3, (sqlite3_int64)time(NULL)); sqlite3_step(stmt); - int changes = sqlite3_changes(mdb->db); + int changes = sqlite3_changes(mdb.db); sqlite3_finalize(stmt); lua_pushboolean(L, changes > 0); @@ -318,25 +295,17 @@ return 1; } static int l_stash_clear(lua_State *L) { - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } - - sqlite3_exec(mdb->db, "DELETE FROM kv;", NULL, NULL, NULL); + check_init(L); + sqlite3_exec(mdb.db, "DELETE FROM kv;", NULL, NULL, NULL); return 0; } static int l_stash_count(lua_State *L) { - MemDB *mdb = &mem_dbs[current_db_idx]; - if (!mdb->db) { - return luaL_error(L, "database not initialized"); - } - + check_init(L); sqlite3_stmt *stmt; const char *sql = "SELECT COUNT(*) FROM kv WHERE (e > ? OR e = 0);"; - if (sqlite3_prepare_v2(mdb->db, sql, -1, &stmt, NULL) != SQLITE_OK) { - return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb->db)); + if (sqlite3_prepare_v2(mdb.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + return luaL_error(L, "failed to prepare statement: %s", sqlite3_errmsg(mdb.db)); } sqlite3_bind_int64(stmt, 1, (sqlite3_int64)time(NULL)); @@ -350,7 +319,7 @@ return 1; } static const struct luaL_Reg stash_lib[] = { - {"use", l_stash_use}, + {"cleanup", l_stash_cleanup}, {"set", l_stash_set}, {"get", l_stash_get}, {"del", l_stash_del},