ascii-chat
Binary v0.15.83-53-gb711d8720
Library lib/v0.3.2-701-gb711d8720
Video chat in your terminal
Loading...
Searching...
No Matches
database.h File Reference

Go to the source code of this file.

Data Structures

struct  database_session_stats_t
 The useful part of database session stats is in participants, host_ready, migrating: together they describe what is pending, who owns it, and how much data is valid. Keep the object with its parent operation and use the parent cleanup path. More...

Macros

#define DISCOVERY_DATABASE_SESSION_STRING_BUFFER_SIZE   64

Typedefs

typedef sqlite3 discovery_database_t
 Discovery database handle (sqlite3 wrapper).

Functions

asciichat_error_t database_session_transport_set (sqlite3 *db, const uint8_t session_id[16], const void *metadata, size_t length)
asciichat_error_t database_session_transport_get (sqlite3 *db, const uint8_t session_id[16], void *metadata, size_t capacity, size_t *length)
asciichat_error_t database_init (const char *db_path, sqlite3 **db)
 Initialize database and create schema.
void database_close (sqlite3 *db)
 Close database.
asciichat_error_t database_session_create (sqlite3 *db, const acip_session_create_t *req, const acds_config_t *config, acip_session_created_t *resp)
 Create new session.
asciichat_error_t database_session_create_keys (sqlite3 *db, const acip_session_create_t *req, const uint8_t(*keys)[32], size_t count, const acds_config_t *config, acip_session_created_t *resp)
asciichat_error_t database_session_create_keys_with_transport (sqlite3 *db, const acip_session_create_t *req, const uint8_t(*keys)[32], size_t count, const acds_config_t *config, acip_session_created_t *resp, const void *metadata, size_t metadata_length)
asciichat_error_t database_session_lookup (sqlite3 *db, const char *session_string, const acds_config_t *config, acip_session_info_t *resp)
 Lookup session by string.
asciichat_error_t database_session_join (sqlite3 *db, const acip_session_join_t *req, const acds_config_t *config, acip_session_joined_t *resp)
 Join existing session.
asciichat_error_t database_session_leave (sqlite3 *db, const uint8_t session_id[16], const uint8_t participant_id[16])
 Leave session.
session_entry_t * database_session_find_by_id (sqlite3 *db, const uint8_t session_id[16])
 Find session by session_id.
session_entry_t * database_session_find_by_string (sqlite3 *db, const char *session_string)
 Find session by session_string.
void database_session_cleanup_expired (sqlite3 *db)
 Clean up inactive sessions.
asciichat_error_t database_session_update_host (sqlite3 *db, const uint8_t session_id[16], const uint8_t host_participant_id[16], const char *host_address, uint16_t host_port, uint8_t connection_type)
 Update session host (discovery mode).
asciichat_error_t database_session_clear_host (sqlite3 *db, const uint8_t session_id[16])
 Clear session host (discovery mode - host migration).
asciichat_error_t database_session_start_migration (sqlite3 *db, const uint8_t session_id[16])
 Start host migration (discovery mode).
bool database_session_is_migration_ready (sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
 Check if migration window has completed.
asciichat_error_t database_session_add_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32], uint32_t key_version)
 Add a key to a session.
asciichat_error_t database_session_revoke_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 Revoke a key from a session.
bool database_session_verify_key (sqlite3 *db, const char *session_string, const uint8_t identity_pubkey[32])
 Check if a key is valid for a session (exists and not revoked).
asciichat_error_t database_session_get_keys (sqlite3 *db, const char *session_string, uint8_t(*keys_out)[32], size_t max_keys, size_t *count_out)
 Get all active keys for a session.
asciichat_error_t database_stats_session_count (sqlite3 *db, int64_t *count_out)
 Get total number of active sessions.
asciichat_error_t database_stats_unique_clients (sqlite3 *db, int64_t *count_out)
 Get total number of unique clients/participants.
asciichat_error_t database_stats_snapshot (sqlite3 *db, database_session_stats_t *rows, size_t capacity, size_t *row_count, uint64_t *sessions, uint64_t *participants)
asciichat_error_t database_session_update_host_with_transport (sqlite3 *db, const uint8_t session[16], const uint8_t host[16], const char *address, uint16_t port, uint8_t type, const void *metadata, size_t length)

Macro Definition Documentation

◆ DISCOVERY_DATABASE_SESSION_STRING_BUFFER_SIZE

#define DISCOVERY_DATABASE_SESSION_STRING_BUFFER_SIZE   64

Definition at line 13 of file database.h.

Typedef Documentation

◆ discovery_database_t

typedef sqlite3 discovery_database_t

Discovery database handle (sqlite3 wrapper).

Definition at line 33 of file database.h.

Function Documentation

◆ database_close()

void database_close ( sqlite3 * db)

Close database.

Parameters
dbDatabase handle to close

Definition at line 305 of file discovery/database.c.

305 {
306 ASCII_OTEL_SCOPE(DATABASE_CLOSE);
307 if (!db) {
308 return;
309 }
310 sqlite3_close(db);
311 log_debug("Database closed");
312}
#define log_debug(...)
Log a DEBUG message.
Definition log/log.h:595
#define ASCII_OTEL_SCOPE(id)
Definition otel.h:72

References ASCII_OTEL_SCOPE, and log_debug.

Referenced by acds_server_shutdown().

◆ database_init()

asciichat_error_t database_init ( const char * db_path,
sqlite3 ** db )

Initialize database and create schema.

Parameters
db_pathPath to SQLite database file
dbOutput database handle
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 188 of file discovery/database.c.

188 {
189 ASCII_OTEL_SCOPE(DATABASE_INIT);
190 if (!db_path || !db) {
191 return SET_ERRNO(ERROR_INVALID_PARAM, "db_path or db is NULL");
192 }
193
194 log_info("Opening database: %s", db_path);
195
196 // Signaling workers share this connection; macOS SQLite defaults to multi-thread mode.
197 int rc = sqlite3_open_v2(db_path, db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, NULL);
198 if (rc != SQLITE_OK) {
199 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
200 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
201 const char *err = sqlite3_errmsg(*db);
202 sqlite3_close(*db);
203 *db = NULL;
204 return SET_ERRNO(ERROR_CONFIG, "Failed to open database: %s", err);
205 }
206
207 const gotel_query_catalog_t query_catalog = {database_queries, DATABASE_QUERY_COUNT, database_query_get};
208 ascii_otel_sqlite_catalog(*db, &query_catalog);
209 // Enable Write-Ahead Logging for concurrent reads
210 char *err_msg = NULL;
211 rc = sqlite3_exec(*db, database_queries[DATABASE_QUERY_EnableWAL].sql, NULL, NULL, &err_msg);
212 if (rc != SQLITE_OK) {
213 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
214 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
215 log_warn("Failed to enable WAL mode: %s", err_msg ? err_msg : "unknown error");
216 sqlite3_free(err_msg);
217 }
218
219 // Enable foreign key constraints
220 rc = sqlite3_exec(*db, database_queries[DATABASE_QUERY_EnableForeignKeys].sql, NULL, NULL, &err_msg);
221 if (rc != SQLITE_OK) {
222 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
223 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
224 log_error("Failed to enable foreign keys: %s", err_msg ? err_msg : "unknown error");
225 sqlite3_free(err_msg);
226 sqlite3_close(*db);
227 *db = NULL;
228 return SET_ERRNO(ERROR_CONFIG, "Failed to enable foreign keys");
229 }
230
231 // Create schema
239 for (size_t i = 0; i < sizeof(schema) / sizeof(schema[0]); ++i) {
240 rc = sqlite3_exec(*db, database_queries[schema[i]].sql, NULL, NULL, &err_msg);
241 if (rc != SQLITE_OK)
242 break;
243 }
244 if (rc != SQLITE_OK) {
245 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
246 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
247 log_error("Failed to create schema: %s", err_msg ? err_msg : "unknown error");
248 sqlite3_free(err_msg);
249 sqlite3_close(*db);
250 *db = NULL;
251 return SET_ERRNO(ERROR_CONFIG, "Failed to create database schema");
252 }
253
254 rc = sqlite3_exec(*db, database_queries[DATABASE_QUERY_SchemaSessionTransports].sql, NULL, NULL, NULL);
255 if (rc == SQLITE_OK)
256 rc = sqlite3_exec(*db, database_queries[DATABASE_QUERY_SchemaSessionTransportHosts].sql, NULL, NULL, NULL);
257 if (rc == SQLITE_OK)
258 rc = sqlite3_exec(*db, database_queries[DATABASE_QUERY_SchemaClearTransportsTrigger].sql, NULL, NULL, NULL);
259 if (rc != SQLITE_OK) {
260 sqlite3_close(*db);
261 *db = NULL;
262 return SET_ERRNO(ERROR_CONFIG, "Cannot create discovery transport metadata table");
263 }
264
265 // Idempotent, transactional migration from the untyped key table. A policy
266 // row is also the migration marker: reopening must never restore revoked keys.
267 sqlite3_stmt *legacy_table = NULL;
268 rc = ascii_otel_sqlite_prepare(*db, database_queries[DATABASE_QUERY_HasLegacySessionKeys].sql, &legacy_table);
269 bool has_legacy_keys = false;
270 if (rc == SQLITE_OK) {
271 rc = sqlite3_step(legacy_table);
272 has_legacy_keys = rc == SQLITE_ROW;
273 if (rc == SQLITE_ROW || rc == SQLITE_DONE)
274 rc = SQLITE_OK;
275 }
276 sqlite3_finalize(legacy_table);
279 for (size_t i = 0; rc == SQLITE_OK && i < sizeof(migration_schema) / sizeof(migration_schema[0]); ++i)
280 rc = sqlite3_exec(*db, database_queries[migration_schema[i]].sql, NULL, NULL, &err_msg);
281 if (rc == SQLITE_OK && has_legacy_keys) {
282 // Read legacy keys only during migration; new databases do not create the
283 // superseded table or its indexes. Existing tables remain intact.
284 const char *legacy_import = database_queries[DATABASE_QUERY_ImportLegacySessionKeys].sql;
285 rc = sqlite3_exec(*db, legacy_import, NULL, NULL, &err_msg);
286 }
290 for (size_t i = 0; rc == SQLITE_OK && i < sizeof(migration_backfill) / sizeof(migration_backfill[0]); ++i)
291 rc = sqlite3_exec(*db, database_queries[migration_backfill[i]].sql, NULL, NULL, &err_msg);
292 if (rc != SQLITE_OK) {
293 log_error("Failed to migrate session keys: %s", err_msg ? err_msg : "unknown error");
294 sqlite3_free(err_msg);
295 sqlite3_exec(*db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
296 sqlite3_close(*db);
297 *db = NULL;
298 return SET_ERRNO(ERROR_CONFIG, "Failed to migrate session keys");
299 }
300
301 log_info("Database initialized successfully (SQLite as single source of truth)");
302 return ASCIICHAT_OK;
303}
#define SET_ERRNO(code,...)
@ ASCIICHAT_OK
Definition error_codes.h:46
@ ERROR_CONFIG
Definition error_codes.h:52
@ ERROR_INVALID_PARAM
#define log_warn(...)
Log a WARN message.
Definition log/log.h:621
#define log_error(...)
Log an ERROR message.
Definition log/log.h:634
#define log_info(...)
Log an INFO message.
Definition log/log.h:608
int ascii_otel_sqlite_prepare(sqlite3 *db, const char *sql, sqlite3_stmt **statement)
Definition otel.c:840
void ascii_otel_sqlite_catalog(sqlite3 *db, const gotel_query_catalog_t *catalog)
Definition otel.c:857
void ascii_otel_metric(ascii_otel_metric_t metric, double value)
Definition otel.c:161
@ DATABASE_QUERY_RollbackTransaction
@ DATABASE_QUERY_EnableForeignKeys
@ DATABASE_QUERY_SchemaSessionTransports
@ DATABASE_QUERY_ImportLegacySessionKeys
@ DATABASE_QUERY_EnableWAL
@ DATABASE_QUERY_SchemaParticipants
@ DATABASE_QUERY_SchemaIdxRateEvents
@ DATABASE_QUERY_SchemaIdentityKeys
@ DATABASE_QUERY_SchemaIdxParticipantsSession
@ DATABASE_QUERY_BackfillSessionKeyPolicy
@ DATABASE_QUERY_SchemaClearTransportsTrigger
@ DATABASE_QUERY_COUNT
@ DATABASE_QUERY_SchemaSessions
@ DATABASE_QUERY_SchemaSessionTransportHosts
@ DATABASE_QUERY_SchemaIdxSessionsId
@ DATABASE_QUERY_BackfillSessionIdentityKeys
@ DATABASE_QUERY_SchemaIdxSessionsActivity
@ DATABASE_QUERY_HasLegacySessionKeys
@ DATABASE_QUERY_CommitTransaction
@ DATABASE_QUERY_SchemaRateEvents
@ DATABASE_QUERY_SchemaKeyPolicy
@ DATABASE_QUERY_BeginTransaction
enum database_query_id database_query_id_t

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_catalog(), ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_BackfillSessionIdentityKeys, DATABASE_QUERY_BackfillSessionKeyPolicy, DATABASE_QUERY_BeginTransaction, DATABASE_QUERY_CommitTransaction, DATABASE_QUERY_COUNT, DATABASE_QUERY_EnableForeignKeys, DATABASE_QUERY_EnableWAL, DATABASE_QUERY_HasLegacySessionKeys, DATABASE_QUERY_ImportLegacySessionKeys, DATABASE_QUERY_RollbackTransaction, DATABASE_QUERY_SchemaClearTransportsTrigger, DATABASE_QUERY_SchemaIdentityKeys, DATABASE_QUERY_SchemaIdxParticipantsSession, DATABASE_QUERY_SchemaIdxRateEvents, DATABASE_QUERY_SchemaIdxSessionsActivity, DATABASE_QUERY_SchemaIdxSessionsId, DATABASE_QUERY_SchemaKeyPolicy, DATABASE_QUERY_SchemaParticipants, DATABASE_QUERY_SchemaRateEvents, DATABASE_QUERY_SchemaSessions, DATABASE_QUERY_SchemaSessionTransportHosts, DATABASE_QUERY_SchemaSessionTransports, ERROR_CONFIG, ERROR_INVALID_PARAM, log_error, log_info, log_warn, and SET_ERRNO.

Referenced by acds_server_init().

◆ database_session_add_key()

asciichat_error_t database_session_add_key ( sqlite3 * db,
const char * session_string,
const uint8_t identity_pubkey[32],
uint32_t key_version )

Add a key to a session.

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key (32 bytes)
key_versionKey version for tracking rotation
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1389 of file discovery/database.c.

1391 {
1392 ASCII_OTEL_SCOPE(DATABASE_ADD_KEY);
1393 if (!db || !session_string || !identity_pubkey || sodium_is_zero(identity_pubkey, 32)) {
1394 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for add_key");
1395 }
1396
1397 const char *sql = database_queries[DATABASE_QUERY_AddSessionKey].sql;
1398
1399 sqlite3_stmt *stmt = NULL;
1400 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1401 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare add_key statement: %s", sqlite3_errmsg(db));
1402 }
1403
1404 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1405 SQLITE_STATIC) != SQLITE_OK) ||
1406 (sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":identity_pubkey"), identity_pubkey, 32,
1407 SQLITE_STATIC) != SQLITE_OK) ||
1408 (sqlite3_bind_int(stmt, sqlite3_bind_parameter_index(stmt, ":key_version"), key_version) != SQLITE_OK) ||
1409 (sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":added_at"), database_get_current_time_ms()) !=
1410 SQLITE_OK)) {
1411 sqlite3_finalize(stmt);
1412 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1413 }
1414
1415 int rc = sqlite3_step(stmt);
1416 sqlite3_finalize(stmt);
1417
1418 if (rc != SQLITE_DONE) {
1419 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1420 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1421 return SET_ERRNO(ERROR_CONFIG, "Failed to add session key: %s", sqlite3_errmsg(db));
1422 }
1423
1424 log_debug("Added key to session %s (version=%u)", session_string, key_version);
1425 return ASCIICHAT_OK;
1426}
@ DATABASE_QUERY_AddSessionKey

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_AddSessionKey, ED25519_PUBLIC_KEY_SIZE, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ database_session_cleanup_expired()

void database_session_cleanup_expired ( sqlite3 * db)

Clean up inactive sessions.

Removes sessions that have been inactive for more than 3 hours. Inactivity is measured by last_activity_at (updated on join/leave/host updates). Called periodically by background cleanup thread.

Parameters
dbDatabase handle

Definition at line 1140 of file discovery/database.c.

1140 {
1141 ASCII_OTEL_SCOPE(DATABASE_SESSION_CLEANUP_EXPIRED);
1142 if (!db) {
1143 return;
1144 }
1145
1146 uint64_t now = database_get_current_time_ms();
1147 // 3 hours in milliseconds
1148 uint64_t inactivity_threshold = 3ULL * SEC_PER_HOUR * MS_PER_SEC_INT;
1149 uint64_t cutoff_time = now - inactivity_threshold;
1150
1151 // Log sessions about to be deleted
1152 const char *log_sql = database_queries[DATABASE_QUERY_FindExpiredSessions].sql;
1153 sqlite3_stmt *stmt = NULL;
1154 if (ascii_otel_sqlite_prepare(db, log_sql, &stmt) == SQLITE_OK) {
1155 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":cutoff_time"), (sqlite3_int64)cutoff_time) !=
1156 SQLITE_OK)) {
1157 sqlite3_finalize(stmt);
1158 log_error("Failed to bind SQLite query parameters");
1159 return;
1160 }
1161 while (sqlite3_step(stmt) == SQLITE_ROW) {
1162 const char *session_string = (const char *)sqlite3_column_text(stmt, 0);
1163 uint64_t last_activity = (uint64_t)sqlite3_column_int64(stmt, 1);
1164 uint64_t inactive_ms = now - last_activity;
1165 uint64_t inactive_hours = inactive_ms / (SEC_PER_HOUR * MS_PER_SEC_INT);
1166 log_info("Session %s inactive for %lu hours, deleting", session_string ? session_string : "<unknown>",
1167 inactive_hours);
1168 }
1169 sqlite3_finalize(stmt);
1170 }
1171
1172 // Delete inactive sessions (CASCADE deletes participants)
1173 const char *del_sql = database_queries[DATABASE_QUERY_DeleteExpiredSessions].sql;
1174 if (ascii_otel_sqlite_prepare(db, del_sql, &stmt) == SQLITE_OK) {
1175 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":cutoff_time"), (sqlite3_int64)cutoff_time) !=
1176 SQLITE_OK)) {
1177 sqlite3_finalize(stmt);
1178 log_error("Failed to bind SQLite query parameters");
1179 return;
1180 }
1181 sqlite3_step(stmt);
1182 int deleted = sqlite3_changes(db);
1183 sqlite3_finalize(stmt);
1184
1185 if (deleted > 0) {
1187 log_info("Cleaned up %d inactive sessions (>3 hours)", deleted);
1188 }
1189 }
1190}
unsigned long long uint64_t
Definition common.h:63
#define SEC_PER_HOUR
Definition time.h:180
#define MS_PER_SEC_INT
Definition time.h:176
@ STATS_COUNTER_SESSION_EXPIRATIONS
void stats_counter_add(stats_scope_t *scope, stats_counter_id_t id, uint64_t amount)
@ DATABASE_QUERY_DeleteExpiredSessions
@ DATABASE_QUERY_FindExpiredSessions
stats_scope_t * stats_runtime_scope(void)
Definition runtime.c:80

References ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), DATABASE_QUERY_DeleteExpiredSessions, DATABASE_QUERY_FindExpiredSessions, log_error, log_info, MS_PER_SEC_INT, SEC_PER_HOUR, stats_counter_add(), STATS_COUNTER_SESSION_EXPIRATIONS, and stats_runtime_scope().

◆ database_session_clear_host()

asciichat_error_t database_session_clear_host ( sqlite3 * db,
const uint8_t session_id[16] )

Clear session host (discovery mode - host migration).

Called when the current host disconnects or fails. Clears host fields so remaining participants can negotiate a new host.

Parameters
dbDatabase handle
session_idSession UUID
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1252 of file discovery/database.c.

1252 {
1253 ASCII_OTEL_SCOPE(DATABASE_SESSION_CLEAR_HOST);
1254 if (!db || !session_id) {
1255 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
1256 }
1257
1258 const char *sql = database_queries[DATABASE_QUERY_ClearHost].sql;
1259
1260 sqlite3_stmt *stmt = NULL;
1261 int rc = ascii_otel_sqlite_prepare(db, sql, &stmt);
1262 if (rc != SQLITE_OK) {
1263 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1264 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1265 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host clear: %s", sqlite3_errmsg(db));
1266 }
1267
1268 if ((sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":session_id"), session_id, 16, SQLITE_STATIC) !=
1269 SQLITE_OK)) {
1270 sqlite3_finalize(stmt);
1271 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1272 }
1273
1274 rc = sqlite3_step(stmt);
1275 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
1276 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
1277 sqlite3_finalize(stmt);
1278
1279 if (rc != SQLITE_DONE) {
1280 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1281 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1282 return SET_ERRNO(ERROR_CONFIG, "Failed to clear host: %s", sqlite3_errmsg(db));
1283 }
1284
1285 int changes = sqlite3_changes(db);
1286 if (changes == 0) {
1287 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host clear");
1288 }
1289
1290 log_info("Session host cleared (session=%02x%02x...) - ready for migration", session_id[0], session_id[1]);
1291
1292 return ASCIICHAT_OK;
1293}
@ ERROR_INVALID_STATE
@ DATABASE_QUERY_ClearHost
uint8_t session_id[SESSION_ID_SIZE]

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_ClearHost, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, SESSION_ID_SIZE, and SET_ERRNO.

◆ database_session_create()

asciichat_error_t database_session_create ( sqlite3 * db,
const acip_session_create_t * req,
const acds_config_t * config,
acip_session_created_t * resp )

Create new session.

Parameters
dbDatabase handle
reqSession creation request
configACDS server configuration
respSession creation response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 434 of file discovery/database.c.

435 {
436 if (!req)
437 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing create request");
438 return database_session_create_keys(db, req, (const uint8_t (*)[32]) & req->identity_pubkey, 1, config, resp);
439}
asciichat_error_t database_session_create_keys(sqlite3 *db, const acip_session_create_t *req, const uint8_t(*keys)[32], size_t count, const acds_config_t *config, acip_session_created_t *resp)
unsigned char uint8_t
Definition common.h:60

References acip_session_create_t, acip_session_created_t, database_session_create_keys(), ERROR_INVALID_PARAM, and SET_ERRNO.

◆ database_session_create_keys()

asciichat_error_t database_session_create_keys ( sqlite3 * db,
const acip_session_create_t * req,
const uint8_t(*) keys[32],
size_t count,
const acds_config_t * config,
acip_session_created_t * resp )

Create an explicit Ed25519 key set atomically. More than one key opts into allowlisted joins; count=1 preserves the legacy admission policy. The caller must verify each create proof before calling this persistence API.

Definition at line 512 of file discovery/database.c.

514 {
515 return database_session_create_keys_with_transport(db, req, keys, count, config, resp, NULL, 0);
516}
asciichat_error_t database_session_create_keys_with_transport(sqlite3 *db, const acip_session_create_t *req, const uint8_t(*keys)[32], size_t count, const acds_config_t *config, acip_session_created_t *resp, const void *metadata, size_t metadata_length)

References acip_session_create_t, acip_session_created_t, and database_session_create_keys_with_transport().

Referenced by database_session_create().

◆ database_session_create_keys_with_transport()

asciichat_error_t database_session_create_keys_with_transport ( sqlite3 * db,
const acip_session_create_t * req,
const uint8_t(*) keys[32],
size_t count,
const acds_config_t * config,
acip_session_created_t * resp,
const void * metadata,
size_t metadata_length )

Persist advertised transports in the same transaction as the session and key set.

Definition at line 496 of file discovery/database.c.

499 {
500 if (!db)
501 return SET_ERRNO(ERROR_INVALID_PARAM, "Missing database");
502 // SQLite serializes statements, but a shared connection also needs to keep
503 // other client threads out of this multi-statement savepoint.
504 sqlite3_mutex *mutex = sqlite3_db_mutex(db);
505 sqlite3_mutex_enter(mutex);
506 asciichat_error_t result =
507 database_session_create_keys_locked(db, req, keys, count, config, resp, metadata, metadata_length);
508 sqlite3_mutex_leave(mutex);
509 return result;
510}
asciichat_error_t
Error and exit codes - unified status values (0-255).
Definition error_codes.h:44

References acip_session_create_t, acip_session_created_t, ERROR_INVALID_PARAM, and SET_ERRNO.

Referenced by database_session_create_keys().

◆ database_session_find_by_id()

session_entry_t * database_session_find_by_id ( sqlite3 * db,
const uint8_t session_id[16] )

Find session by session_id.

Allocates and returns a session_entry_t. Caller must free with session_entry_destroy().

Parameters
dbDatabase handle
session_idSession UUID to find
Returns
Allocated session entry (caller owns), or NULL if not found

Definition at line 1078 of file discovery/database.c.

1078 {
1079 if (!db || !session_id) {
1080 return NULL;
1081 }
1082
1083 const char *sql = database_queries[DATABASE_QUERY_GetSessionById].sql;
1084
1085 sqlite3_stmt *stmt = NULL;
1086 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1087 return NULL;
1088 }
1089
1090 if ((sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":session_id"), session_id, 16, SQLITE_STATIC) !=
1091 SQLITE_OK)) {
1092 sqlite3_finalize(stmt);
1093 log_error("Failed to bind SQLite query parameters");
1094 return NULL;
1095 }
1096
1097 session_entry_t *session = NULL;
1098 if (sqlite3_step(stmt) == SQLITE_ROW) {
1099 session = load_session_from_row(stmt);
1100 if (session) {
1101 load_session_participants(db, session);
1102 }
1103 }
1104
1105 sqlite3_finalize(stmt);
1106 return session;
1107}
struct session_entry session_entry_t
Persisted discovery-session record used to admit and route participants.
@ DATABASE_QUERY_GetSessionById

References ascii_otel_sqlite_prepare(), DATABASE_QUERY_GetSessionById, log_error, session_id, and SESSION_ID_SIZE.

Referenced by acds_send_current_host(), signaling_broadcast(), signaling_relay_ice(), and signaling_relay_sdp().

◆ database_session_find_by_string()

session_entry_t * database_session_find_by_string ( sqlite3 * db,
const char * session_string )

Find session by session_string.

Allocates and returns a session_entry_t. Caller must free with session_entry_destroy().

Parameters
dbDatabase handle
session_stringSession string to find
Returns
Allocated session entry (caller owns), or NULL if not found

Definition at line 1109 of file discovery/database.c.

1109 {
1110 if (!db || !session_string) {
1111 return NULL;
1112 }
1113
1114 const char *sql = database_queries[DATABASE_QUERY_GetSessionByString].sql;
1115
1116 sqlite3_stmt *stmt = NULL;
1117 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1118 return NULL;
1119 }
1120
1121 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1122 SQLITE_STATIC) != SQLITE_OK)) {
1123 sqlite3_finalize(stmt);
1124 log_error("Failed to bind SQLite query parameters");
1125 return NULL;
1126 }
1127
1128 session_entry_t *session = NULL;
1129 if (sqlite3_step(stmt) == SQLITE_ROW) {
1130 session = load_session_from_row(stmt);
1131 if (session) {
1132 load_session_participants(db, session);
1133 }
1134 }
1135
1136 sqlite3_finalize(stmt);
1137 return session;
1138}
@ DATABASE_QUERY_GetSessionByString

References ascii_otel_sqlite_prepare(), DATABASE_QUERY_GetSessionByString, and log_error.

Referenced by database_session_join().

◆ database_session_get_keys()

asciichat_error_t database_session_get_keys ( sqlite3 * db,
const char * session_string,
uint8_t(*) keys_out[32],
size_t max_keys,
size_t * count_out )

Get all active keys for a session.

Parameters
dbDatabase handle
session_stringSession string
keys_outOutput array for keys (caller must provide buffer)
max_keysMaximum number of keys to retrieve
count_outOutput: number of keys retrieved
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1492 of file discovery/database.c.

1493 {
1494 ASCII_OTEL_SCOPE(DATABASE_GET_KEYS);
1495 if (!db || !session_string || !keys_out || !count_out) {
1496 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for get_keys");
1497 }
1498
1499 const char *sql = database_queries[DATABASE_QUERY_GetSessionKeys].sql;
1500
1501 sqlite3_stmt *stmt = NULL;
1502 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1503 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare get_keys statement: %s", sqlite3_errmsg(db));
1504 }
1505
1506 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1507 SQLITE_STATIC) != SQLITE_OK)) {
1508 sqlite3_finalize(stmt);
1509 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1510 }
1511
1512 size_t count = 0;
1513 while (sqlite3_step(stmt) == SQLITE_ROW && count < max_keys) {
1514 const void *blob = sqlite3_column_blob(stmt, 0);
1515 if (blob && sqlite3_column_bytes(stmt, 0) == 32) {
1516 memcpy(keys_out[count], blob, ED25519_PUBLIC_KEY_SIZE);
1517 count++;
1518 }
1519 }
1520
1521 sqlite3_finalize(stmt);
1522 *count_out = count;
1523
1524 log_debug("Retrieved %zu keys for session %s", count, session_string);
1525 return ASCIICHAT_OK;
1526}
@ DATABASE_QUERY_GetSessionKeys
#define ED25519_PUBLIC_KEY_SIZE
Ed25519 public key size in bytes.
Definition sizes.h:33

References ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_GetSessionKeys, ED25519_PUBLIC_KEY_SIZE, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ database_session_is_migration_ready()

bool database_session_is_migration_ready ( sqlite3 * db,
const uint8_t session_id[16],
uint64_t migration_window_ms )

Check if migration window has completed.

Called periodically to check if migration collection window timeout has expired. Returns the session if migration is ready for host election.

Parameters
dbDatabase handle
session_idSession UUID
migration_window_msCollection window duration in milliseconds
Returns
true if collection window has completed, false otherwise

Definition at line 1343 of file discovery/database.c.

1344 {
1345 ASCII_OTEL_SCOPE(DATABASE_MIGRATION_READY);
1346 if (!db || !session_id) {
1347 return false;
1348 }
1349
1350 uint64_t now_ns = time_get_realtime_ns();
1351 const char *sql = database_queries[DATABASE_QUERY_GetMigrationState].sql;
1352
1353 sqlite3_stmt *stmt = NULL;
1354 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1355 return false;
1356 }
1357
1358 if ((sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":session_id"), session_id, 16, SQLITE_STATIC) !=
1359 SQLITE_OK)) {
1360 sqlite3_finalize(stmt);
1361 log_error("Failed to bind SQLite query parameters");
1362 return false;
1363 }
1364
1365 bool ready = false;
1366 if (sqlite3_step(stmt) == SQLITE_ROW) {
1367 int in_migration = sqlite3_column_int(stmt, 0);
1368 uint64_t migration_start_ns = (uint64_t)sqlite3_column_int64(stmt, 1);
1369
1370 if (in_migration) {
1371 uint64_t elapsed_ns = now_ns - migration_start_ns;
1372 uint64_t migration_window_ns = migration_window_ms * NS_PER_MS_INT;
1373 if (elapsed_ns >= migration_window_ns) {
1374 ready = true;
1375 log_debug("Migration window complete (session=%02x%02x..., elapsed=%lu ns, window=%lu ns)", session_id[0],
1376 session_id[1], elapsed_ns, migration_window_ns);
1377 }
1378 }
1379 }
1380
1381 sqlite3_finalize(stmt);
1382 return ready;
1383}
#define NS_PER_MS_INT
Definition time.h:168
@ DATABASE_QUERY_GetMigrationState

References ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), DATABASE_QUERY_GetMigrationState, log_debug, log_error, NS_PER_MS_INT, session_id, and SESSION_ID_SIZE.

Referenced by database_session_update_host().

◆ database_session_join()

asciichat_error_t database_session_join ( sqlite3 * db,
const acip_session_join_t * req,
const acds_config_t * config,
acip_session_joined_t * resp )

Join existing session.

Parameters
dbDatabase handle
reqJoin request
configACDS server configuration (for TURN credentials)
respJoin response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 834 of file discovery/database.c.

835 {
836 ASCII_OTEL_SCOPE(DATABASE_SESSION_JOIN);
837 if (!db || !req || !config || !resp) {
838 return SET_ERRNO(ERROR_INVALID_PARAM, "db, req, config, or resp is NULL");
839 }
840
841 memset(resp, 0, sizeof(*resp));
842 resp->success = 0;
843
844 if (req->session_string_len == 0 || req->session_string_len >= sizeof(req->session_string) ||
845 memchr(req->session_string, '\0', (size_t)req->session_string_len) ||
846 req->session_string[req->session_string_len] != '\0' ||
847 (req->has_password && !memchr(req->password, '\0', sizeof(req->password))))
848 return SET_ERRNO(ERROR_INVALID_PARAM, "Malformed join request");
849
850 // Extract session string
851 char session_string[ACIP_MAX_SESSION_STRING_LEN] = {0};
852 size_t len = req->session_string_len < (ACIP_MAX_SESSION_STRING_LEN - 1) ? req->session_string_len
854 memcpy(session_string, req->session_string, len);
855 session_string[len] = '\0';
856
857 // Find session
858 session_entry_t *session = database_session_find_by_string(db, session_string);
859 if (!session) {
860 ascii_otel_metric(ASCII_OTEL_METRIC_LOOKUP_MISS, 1);
861 resp->error_code = ACIP_ERROR_SESSION_NOT_FOUND;
862 SAFE_STRNCPY(resp->error_message, "Session not found", sizeof(resp->error_message));
863 log_warn("Session join failed: %s (not found)", session_string);
864 return ASCIICHAT_OK;
865 }
866
867 // Reject unauthorized identities before spending Argon2 resources or revealing
868 // a password result. This preliminary check is repeated in the transaction.
869 sqlite3_mutex *mutex = sqlite3_db_mutex(db);
870 sqlite3_mutex_enter(mutex);
871 asciichat_error_t result = database_session_authorize_join(db, req, config, session_string, resp);
872 sqlite3_mutex_leave(mutex);
873 if (result != ASCIICHAT_OK || resp->error_code != ACIP_ERROR_NONE) {
874 session_entry_destroy(session);
875 return result;
876 }
877
878 // Check if session is full
879 if (session->current_participants >= session->max_participants) {
880 session_entry_destroy(session);
881 resp->error_code = ACIP_ERROR_SESSION_FULL;
882 SAFE_STRNCPY(resp->error_message, "Session is full", sizeof(resp->error_message));
883 log_warn("Session join failed: %s (full)", session_string);
884 return ASCIICHAT_OK;
885 }
886
887 // Verify password if required
888 if (session->has_password && req->has_password) {
889 if (!verify_password(req->password, session->password_hash)) {
890 session_entry_destroy(session);
891 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
892 SAFE_STRNCPY(resp->error_message, "Invalid password", sizeof(resp->error_message));
893 log_warn("Session join failed: %s (invalid password)", session_string);
894 return ASCIICHAT_OK;
895 }
896 } else if (session->has_password && !req->has_password) {
897 session_entry_destroy(session);
898 resp->error_code = ACIP_ERROR_INVALID_PASSWORD;
899 SAFE_STRNCPY(resp->error_message, "Password required", sizeof(resp->error_message));
900 log_warn("Session join failed: %s (password required)", session_string);
901 return ASCIICHAT_OK;
902 }
903
904 // The immutable snapshot records which session/password was verified. The
905 // commit helper reloads mutable state after BEGIN and never hashes under lock.
906 session_entry_t *joined = NULL;
907 sqlite3_mutex_enter(mutex);
908 result = database_session_join_commit(db, req, config, session, resp, &joined);
909 sqlite3_mutex_leave(mutex);
910 session_entry_destroy(session);
911 if (joined)
912 database_session_join_response(joined, config, resp);
913 return result;
914}
#define ACIP_MAX_SESSION_STRING_LEN
Definition acds_sizes.h:6
session_entry_t * database_session_find_by_string(sqlite3 *db, const char *session_string)
Find session by session_string.
#define SAFE_STRNCPY(dst, src, size)
Definition common.h:423
@ ACIP_ERROR_INVALID_PASSWORD
Password verification failed.
Definition acds.h:1077
@ ACIP_ERROR_SESSION_NOT_FOUND
Session does not exist.
Definition acds.h:1075
@ ACIP_ERROR_SESSION_FULL
Session has reached max participants.
Definition acds.h:1076
@ ACIP_ERROR_NONE
No error (success).
Definition acds.h:1074
void session_entry_destroy(session_entry_t *entry)
Free a session entry and all its resources.
bool has_password
Password protection flag.
uint8_t max_participants
1-128
char password_hash[ACIP_PASSWORD_HASH_SIZE]
Argon2id hash (if has_password).
uint8_t current_participants
Active participant count.

References ACIP_ERROR_INVALID_PASSWORD, ACIP_ERROR_NONE, ACIP_ERROR_SESSION_FULL, ACIP_ERROR_SESSION_NOT_FOUND, ACIP_MAX_SESSION_STRING_LEN, acip_session_join_t, acip_session_joined_t, ascii_otel_metric(), ASCII_OTEL_SCOPE, ASCIICHAT_OK, session_entry::current_participants, database_session_find_by_string(), ERROR_INVALID_PARAM, session_entry::has_password, log_warn, session_entry::max_participants, session_entry::password_hash, SAFE_STRNCPY, session_entry_destroy(), and SET_ERRNO.

◆ database_session_leave()

asciichat_error_t database_session_leave ( sqlite3 * db,
const uint8_t session_id[16],
const uint8_t participant_id[16] )

Leave session.

Parameters
dbDatabase handle
session_idSession UUID
participant_idParticipant UUID
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 916 of file discovery/database.c.

917 {
918 ASCII_OTEL_SCOPE(DATABASE_SESSION_LEAVE);
919 if (!db || !session_id || !participant_id) {
920 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or participant_id is NULL");
921 }
922
923 // Look up session_string from session_id
924 char session_string[SESSION_STRING_BUFFER_SIZE] = {0};
925 const char *lookup_sql = database_queries[DATABASE_QUERY_LookupSessionString].sql;
926 sqlite3_stmt *stmt = NULL;
927 int rc = ascii_otel_sqlite_prepare(db, lookup_sql, &stmt);
928 if (rc != SQLITE_OK) {
929 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
930 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
931 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
932 }
933
934 int session_id_index = sqlite3_bind_parameter_index(stmt, ":session_id");
935 rc = session_id_index ? sqlite3_bind_blob(stmt, session_id_index, session_id, SESSION_ID_SIZE, SQLITE_STATIC)
936 : SQLITE_RANGE;
937 if (rc != SQLITE_OK) {
938 sqlite3_finalize(stmt);
939 return SET_ERRNO(ERROR_CONFIG, "Failed to bind session lookup parameter");
940 }
941 rc = sqlite3_step(stmt);
942 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
943 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
944 if (rc != SQLITE_ROW) {
945 sqlite3_finalize(stmt);
946 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found");
947 }
948
949 const char *str = (const char *)sqlite3_column_text(stmt, 0);
950 if (str) {
951 SAFE_STRNCPY(session_string, str, sizeof(session_string));
952 }
953 sqlite3_finalize(stmt);
954
955 if (session_string[0] == '\0') {
956 return SET_ERRNO(ERROR_INVALID_STATE, "Session string is empty");
957 }
958
959 // Begin transaction
960 char *err_msg = NULL;
961 rc = sqlite3_exec(db, database_queries[DATABASE_QUERY_BeginTransaction].sql, NULL, NULL, &err_msg);
962 if (rc != SQLITE_OK) {
963 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
964 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
965 log_error("Failed to begin transaction: %s", err_msg ? err_msg : "unknown");
966 sqlite3_free(err_msg);
967 return SET_ERRNO(ERROR_CONFIG, "Failed to begin transaction");
968 }
969
970 // Delete participant
971 const char *del_sql = database_queries[DATABASE_QUERY_DeleteParticipant].sql;
972 rc = ascii_otel_sqlite_prepare(db, del_sql, &stmt);
973 if (rc != SQLITE_OK) {
974 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
975 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
976 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
977 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare participant delete: %s", sqlite3_errmsg(db));
978 }
979
980 if ((sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":participant_id"), participant_id, 16,
981 SQLITE_STATIC) != SQLITE_OK) ||
982 (sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
983 SQLITE_STATIC) != SQLITE_OK)) {
984 sqlite3_finalize(stmt);
985 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
986 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
987 }
988 rc = sqlite3_step(stmt);
989 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
990 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
991 sqlite3_finalize(stmt);
992
993 if (rc != SQLITE_DONE) {
994 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
995 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
996 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
997 return SET_ERRNO(ERROR_CONFIG, "Failed to delete participant: %s", sqlite3_errmsg(db));
998 }
999
1000 int changes = sqlite3_changes(db);
1001 if (changes == 0) {
1002 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
1003 return SET_ERRNO(ERROR_INVALID_STATE, "Participant not in session");
1004 }
1005
1006 uint64_t now = database_get_current_time_ms();
1007
1008 // Decrement participant count and update last activity
1009 const char *update_sql = database_queries[DATABASE_QUERY_DecrementParticipants].sql;
1010 rc = ascii_otel_sqlite_prepare(db, update_sql, &stmt);
1011 if (rc == SQLITE_OK) {
1012 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":last_activity_at"), (sqlite3_int64)now) !=
1013 SQLITE_OK) ||
1014 (sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1015 SQLITE_STATIC) != SQLITE_OK)) {
1016 sqlite3_finalize(stmt);
1017 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
1018 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1019 }
1020 sqlite3_step(stmt);
1021 sqlite3_finalize(stmt);
1022 }
1023
1024 // Check if session is now empty and delete if so
1025 const char *check_sql = database_queries[DATABASE_QUERY_GetParticipantCount].sql;
1026 rc = ascii_otel_sqlite_prepare(db, check_sql, &stmt);
1027 if (rc == SQLITE_OK) {
1028 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1029 SQLITE_STATIC) != SQLITE_OK)) {
1030 sqlite3_finalize(stmt);
1031 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
1032 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1033 }
1034 if (sqlite3_step(stmt) == SQLITE_ROW) {
1035 int count = sqlite3_column_int(stmt, 0);
1036
1037 if (count <= 0) {
1038 log_info("Session %s has no participants, deleting", session_string);
1039 sqlite3_finalize(stmt);
1040
1041 // Delete empty session
1042 const char *del_session_sql = database_queries[DATABASE_QUERY_DeleteSession].sql;
1043 rc = ascii_otel_sqlite_prepare(db, del_session_sql, &stmt);
1044 if (rc == SQLITE_OK) {
1045 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1046 SQLITE_STATIC) != SQLITE_OK)) {
1047 sqlite3_finalize(stmt);
1048 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
1049 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1050 }
1051 sqlite3_step(stmt);
1052 sqlite3_finalize(stmt);
1053 }
1054 stmt = NULL;
1055 } else {
1056 log_info("Participant left session %s (participants=%d remaining)", session_string, count);
1057 }
1058 }
1059 if (stmt) {
1060 sqlite3_finalize(stmt);
1061 }
1062 }
1063
1064 // Commit transaction
1065 rc = sqlite3_exec(db, database_queries[DATABASE_QUERY_CommitTransaction].sql, NULL, NULL, &err_msg);
1066 if (rc != SQLITE_OK) {
1067 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1068 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1069 log_error("Failed to commit transaction: %s", err_msg ? err_msg : "unknown");
1070 sqlite3_free(err_msg);
1071 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackTransaction].sql, NULL, NULL, NULL);
1072 return SET_ERRNO(ERROR_CONFIG, "Failed to commit transaction");
1073 }
1074
1075 return ASCIICHAT_OK;
1076}
#define SESSION_ID_SIZE
@ DATABASE_QUERY_GetParticipantCount
@ DATABASE_QUERY_LookupSessionString
@ DATABASE_QUERY_DeleteParticipant
@ DATABASE_QUERY_DecrementParticipants
@ DATABASE_QUERY_DeleteSession
uint8_t participant_id[SESSION_ID_SIZE]
#define SESSION_STRING_BUFFER_SIZE

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_BeginTransaction, DATABASE_QUERY_CommitTransaction, DATABASE_QUERY_DecrementParticipants, DATABASE_QUERY_DeleteParticipant, DATABASE_QUERY_DeleteSession, DATABASE_QUERY_GetParticipantCount, DATABASE_QUERY_LookupSessionString, DATABASE_QUERY_RollbackTransaction, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_error, log_info, participant_id, SAFE_STRNCPY, session_id, SESSION_ID_SIZE, SESSION_STRING_BUFFER_SIZE, and SET_ERRNO.

◆ database_session_lookup()

asciichat_error_t database_session_lookup ( sqlite3 * db,
const char * session_string,
const acds_config_t * config,
acip_session_info_t * resp )

Lookup session by string.

Parameters
dbDatabase handle
session_stringSession string to look up
configACDS server configuration (for policy flags)
respSession info response (output)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 518 of file discovery/database.c.

519 {
520 ASCII_OTEL_SCOPE(DATABASE_SESSION_LOOKUP);
521 if (!db || !session_string || !config || !resp) {
522 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_string, config, or resp is NULL");
523 }
524
525 memset(resp, 0, sizeof(*resp));
526
527 const char *sql = database_queries[DATABASE_QUERY_LookupSession].sql;
528
529 sqlite3_stmt *stmt = NULL;
530 int rc = ascii_otel_sqlite_prepare(db, sql, &stmt);
531 if (rc != SQLITE_OK) {
532 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
533 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
534 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare session lookup: %s", sqlite3_errmsg(db));
535 }
536
537 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
538 SQLITE_STATIC) != SQLITE_OK)) {
539 sqlite3_finalize(stmt);
540 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
541 }
542
543 rc = sqlite3_step(stmt);
544 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
545 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
546 if (rc != SQLITE_ROW) {
547 sqlite3_finalize(stmt);
548 resp->found = 0;
549 log_debug("Session lookup failed: %s (not found)", session_string);
550 return ASCIICHAT_OK;
551 }
552
553 // Load session data
554 resp->found = 1;
555
556 const void *blob = sqlite3_column_blob(stmt, 1);
557 if (blob) {
558 memcpy(resp->session_id, blob, SESSION_ID_SIZE);
559 }
560
561 blob = sqlite3_column_blob(stmt, 2);
562 if (blob) {
563 memcpy(resp->host_pubkey, blob, ED25519_PUBLIC_KEY_SIZE);
564 }
565
566 resp->max_participants = (uint8_t)sqlite3_column_int(stmt, 4);
567 resp->current_participants = (uint8_t)sqlite3_column_int(stmt, 5);
568 resp->capabilities = (uint8_t)sqlite3_column_int(stmt, 6);
569 resp->has_password = sqlite3_column_int(stmt, 7) != 0;
570 resp->session_type = (uint8_t)sqlite3_column_int(stmt, 9);
571 resp->created_at = (uint64_t)sqlite3_column_int64(stmt, 12);
572 resp->expires_at = (uint64_t)sqlite3_column_int64(stmt, 13);
573
574 // ACDS policy flags
575 resp->require_server_verify = config->require_server_verify ? 1 : 0;
576 resp->require_client_verify = config->require_client_verify ? 1 : 0;
577
578 sqlite3_finalize(stmt);
579
580 log_debug("Session lookup: %s (found, participants=%d/%d)", session_string, resp->current_participants,
581 resp->max_participants);
582
583 return ASCIICHAT_OK;
584}
@ DATABASE_QUERY_LookupSession
bool require_server_verify
ACDS policy: require servers to verify client identity during handshake.
bool require_client_verify
ACDS policy: require clients to verify server identity during handshake.

References acip_session_info_t, ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_LookupSession, ED25519_PUBLIC_KEY_SIZE, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, acds_config_t::require_client_verify, acds_config_t::require_server_verify, SESSION_ID_SIZE, and SET_ERRNO.

◆ database_session_revoke_key()

asciichat_error_t database_session_revoke_key ( sqlite3 * db,
const char * session_string,
const uint8_t identity_pubkey[32] )

Revoke a key from a session.

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key to revoke (32 bytes)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1428 of file discovery/database.c.

1429 {
1430 ASCII_OTEL_SCOPE(DATABASE_REVOKE_KEY);
1431 if (!db || !session_string || !identity_pubkey) {
1432 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid parameters for revoke_key");
1433 }
1434
1435 const char *sql = database_queries[DATABASE_QUERY_RevokeSessionKey].sql;
1436
1437 sqlite3_stmt *stmt = NULL;
1438 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1439 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare revoke_key statement: %s", sqlite3_errmsg(db));
1440 }
1441
1442 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1443 SQLITE_STATIC) != SQLITE_OK) ||
1444 (sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":identity_pubkey"), identity_pubkey, 32,
1445 SQLITE_STATIC) != SQLITE_OK)) {
1446 sqlite3_finalize(stmt);
1447 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1448 }
1449
1450 int rc = sqlite3_step(stmt);
1451 sqlite3_finalize(stmt);
1452
1453 if (rc != SQLITE_DONE) {
1454 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1455 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1456 return SET_ERRNO(ERROR_CONFIG, "Failed to revoke session key: %s", sqlite3_errmsg(db));
1457 }
1458
1459 log_debug("Revoked key from session %s", session_string);
1460 return ASCIICHAT_OK;
1461}
@ DATABASE_QUERY_RevokeSessionKey

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_RevokeSessionKey, ED25519_PUBLIC_KEY_SIZE, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ database_session_start_migration()

asciichat_error_t database_session_start_migration ( sqlite3 * db,
const uint8_t session_id[16] )

Start host migration (discovery mode).

Called when the current host disconnects. Marks session as in_migration and starts a collection window for HOST_LOST candidates.

Parameters
dbDatabase handle
session_idSession UUID
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1295 of file discovery/database.c.

1295 {
1296 ASCII_OTEL_SCOPE(DATABASE_SESSION_START_MIGRATION);
1297 if (!db || !session_id) {
1298 return SET_ERRNO(ERROR_INVALID_PARAM, "db or session_id is NULL");
1299 }
1300
1301 uint64_t now_ns = time_get_realtime_ns();
1302
1303 const char *sql = database_queries[DATABASE_QUERY_StartMigration].sql;
1304
1305 sqlite3_stmt *stmt = NULL;
1306 int rc = ascii_otel_sqlite_prepare(db, sql, &stmt);
1307 if (rc != SQLITE_OK) {
1308 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1309 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1310 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare migration start: %s", sqlite3_errmsg(db));
1311 }
1312
1313 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":migration_start_ns"), (sqlite3_int64)now_ns) !=
1314 SQLITE_OK) ||
1315 (sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":session_id"), session_id, 16, SQLITE_STATIC) !=
1316 SQLITE_OK)) {
1317 sqlite3_finalize(stmt);
1318 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1319 }
1320
1321 rc = sqlite3_step(stmt);
1322 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
1323 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
1324 sqlite3_finalize(stmt);
1325
1326 if (rc != SQLITE_DONE) {
1327 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1328 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1329 return SET_ERRNO(ERROR_CONFIG, "Failed to start migration: %s", sqlite3_errmsg(db));
1330 }
1331
1332 int changes = sqlite3_changes(db);
1333 if (changes == 0) {
1334 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for migration start");
1335 }
1336
1337 log_info("Session migration started (session=%02x%02x..., start_ns=%" PRIu64 ")", session_id[0], session_id[1],
1338 now_ns);
1339
1340 return ASCIICHAT_OK;
1341}
@ DATABASE_QUERY_StartMigration

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_StartMigration, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, SESSION_ID_SIZE, and SET_ERRNO.

◆ database_session_transport_get()

asciichat_error_t database_session_transport_get ( sqlite3 * db,
const uint8_t session_id[16],
void * metadata,
size_t capacity,
size_t * length )

Definition at line 1660 of file discovery/database.c.

1661 {
1662 if (!db || !session_id || !metadata || !length)
1663 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery transport metadata output");
1664 *length = 0;
1665 sqlite3_stmt *statement = NULL;
1666 int rc = ascii_otel_sqlite_prepare(db, database_queries[DATABASE_QUERY_GetSessionTransports].sql, &statement);
1667 if (rc == SQLITE_OK)
1668 rc = sqlite3_bind_blob(statement, sqlite3_bind_parameter_index(statement, ":session_id"), session_id,
1669 SESSION_ID_SIZE, SQLITE_TRANSIENT);
1670 if (rc == SQLITE_OK)
1671 rc = sqlite3_step(statement);
1673 if (rc == SQLITE_ROW) {
1674 int bytes = sqlite3_column_bytes(statement, 0);
1675 if (bytes < 10 || (size_t)bytes > capacity)
1676 result = SET_ERRNO(ERROR_NETWORK_SIZE, "Invalid stored discovery transport metadata size");
1677 else {
1678 memcpy(metadata, sqlite3_column_blob(statement, 0), (size_t)bytes);
1679 *length = (size_t)bytes;
1680 }
1681 } else if (rc != SQLITE_DONE) {
1682 result = SET_ERRNO(ERROR_CONFIG, "Cannot read discovery transport metadata");
1683 }
1684 sqlite3_finalize(statement);
1685 return result;
1686}
@ ERROR_NETWORK_SIZE
Definition error_codes.h:77
@ DATABASE_QUERY_GetSessionTransports

References ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_GetSessionTransports, ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_NETWORK_SIZE, session_id, SESSION_ID_SIZE, and SET_ERRNO.

◆ database_session_transport_set()

asciichat_error_t database_session_transport_set ( sqlite3 * db,
const uint8_t session_id[16],
const void * metadata,
size_t length )

Definition at line 1642 of file discovery/database.c.

1643 {
1644 if (!db || !session_id || !metadata || length < 10 || length > 521)
1645 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid discovery transport metadata");
1646 sqlite3_stmt *statement = NULL;
1647 int rc = ascii_otel_sqlite_prepare(db, database_queries[DATABASE_QUERY_SetSessionTransports].sql, &statement);
1648 if (rc == SQLITE_OK)
1649 rc = sqlite3_bind_blob(statement, sqlite3_bind_parameter_index(statement, ":session_id"), session_id,
1650 SESSION_ID_SIZE, SQLITE_TRANSIENT);
1651 if (rc == SQLITE_OK)
1652 rc = sqlite3_bind_blob(statement, sqlite3_bind_parameter_index(statement, ":metadata"), metadata, (int)length,
1653 SQLITE_TRANSIENT);
1654 if (rc == SQLITE_OK)
1655 rc = sqlite3_step(statement);
1656 sqlite3_finalize(statement);
1657 return rc == SQLITE_DONE ? ASCIICHAT_OK : SET_ERRNO(ERROR_CONFIG, "Cannot store discovery transport metadata");
1658}
@ DATABASE_QUERY_SetSessionTransports

References ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_SetSessionTransports, ERROR_CONFIG, ERROR_INVALID_PARAM, session_id, SESSION_ID_SIZE, and SET_ERRNO.

Referenced by database_session_update_host_with_transport().

◆ database_session_update_host()

asciichat_error_t database_session_update_host ( sqlite3 * db,
const uint8_t session_id[16],
const uint8_t host_participant_id[16],
const char * host_address,
uint16_t host_port,
uint8_t connection_type )

Update session host (discovery mode).

Called when a participant announces they are hosting. Updates the session's host fields so new joiners know where to connect.

Parameters
dbDatabase handle
session_idSession UUID
host_participant_idHost's participant UUID
host_addressHost's reachable address
host_portHost's port
connection_typeHow to reach host (ACIP_CONNECTION_TYPE_*)
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1192 of file discovery/database.c.

1194 {
1195 ASCII_OTEL_SCOPE(DATABASE_SESSION_UPDATE_HOST);
1196 if (!db || !session_id || !host_participant_id) {
1197 return SET_ERRNO(ERROR_INVALID_PARAM, "db, session_id, or host_participant_id is NULL");
1198 }
1199
1200 bool migrating = database_session_is_migration_ready(db, session_id, 0);
1201 uint64_t now = database_get_current_time_ms();
1202
1203 const char *sql = database_queries[DATABASE_QUERY_UpdateHost].sql;
1204
1205 sqlite3_stmt *stmt = NULL;
1206 int rc = ascii_otel_sqlite_prepare(db, sql, &stmt);
1207 if (rc != SQLITE_OK) {
1208 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1209 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1210 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare host update: %s", sqlite3_errmsg(db));
1211 }
1212
1213 if ((sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":host_participant_id"), host_participant_id, 16,
1214 SQLITE_STATIC) != SQLITE_OK) ||
1215 (sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":host_address"), host_address ? host_address : "",
1216 -1, SQLITE_STATIC) != SQLITE_OK) ||
1217 (sqlite3_bind_int(stmt, sqlite3_bind_parameter_index(stmt, ":host_port"), host_port) != SQLITE_OK) ||
1218 (sqlite3_bind_int(stmt, sqlite3_bind_parameter_index(stmt, ":host_connection_type"), connection_type) !=
1219 SQLITE_OK) ||
1220 (sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":last_activity_at"), (sqlite3_int64)now) !=
1221 SQLITE_OK) ||
1222 (sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":session_id"), session_id, 16, SQLITE_STATIC) !=
1223 SQLITE_OK)) {
1224 sqlite3_finalize(stmt);
1225 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1226 }
1227
1228 rc = sqlite3_step(stmt);
1229 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
1230 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
1231 sqlite3_finalize(stmt);
1232
1233 if (rc != SQLITE_DONE) {
1234 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1235 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1236 return SET_ERRNO(ERROR_CONFIG, "Failed to update host: %s", sqlite3_errmsg(db));
1237 }
1238
1239 int changes = sqlite3_changes(db);
1240 if (changes == 0) {
1241 return SET_ERRNO(ERROR_INVALID_STATE, "Session not found for host update");
1242 }
1243
1244 log_info("Session host updated: participant=%02x%02x..., address=%s:%u, type=%d", host_participant_id[0],
1245 host_participant_id[1], host_address ? host_address : "(none)", host_port, connection_type);
1246
1247 if (migrating)
1249 return ASCIICHAT_OK;
1250}
bool database_session_is_migration_ready(sqlite3 *db, const uint8_t session_id[16], uint64_t migration_window_ms)
Check if migration window has completed.
@ DATABASE_QUERY_UpdateHost

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_UpdateHost, database_session_is_migration_ready(), ERROR_CONFIG, ERROR_INVALID_PARAM, ERROR_INVALID_STATE, log_info, session_id, SESSION_ID_SIZE, SET_ERRNO, stats_counter_add(), STATS_COUNTER_HOST_MIGRATIONS, and stats_runtime_scope().

Referenced by database_session_update_host_with_transport().

◆ database_session_update_host_with_transport()

asciichat_error_t database_session_update_host_with_transport ( sqlite3 * db,
const uint8_t session[16],
const uint8_t host[16],
const char * address,
uint16_t port,
uint8_t type,
const void * metadata,
size_t length )

Definition at line 1691 of file discovery/database.c.

1694 {
1695 if (!db || !session || !host || (!metadata && length) || (length && (length < 10 || length > 521)))
1696 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid host transport update");
1697 // Savepoints belong to the shared connection, so exclude other client
1698 // threads until this entire refresh has committed or rolled back.
1699 sqlite3_mutex *mutex = sqlite3_db_mutex(db);
1700 sqlite3_mutex_enter(mutex);
1701 if (sqlite3_exec(db, database_queries[DATABASE_QUERY_BeginHostTransportRefresh].sql, NULL, NULL, NULL) != SQLITE_OK) {
1702 sqlite3_mutex_leave(mutex);
1703 return SET_ERRNO(ERROR_CONFIG, "Cannot begin host transport update");
1704 }
1705 asciichat_error_t result = database_session_update_host(db, session, host, address, port, type);
1706 if (result == ASCIICHAT_OK && length)
1707 result = database_session_transport_set(db, session, metadata, length);
1708 if (result == ASCIICHAT_OK && length) {
1709 sqlite3_stmt *statement = NULL;
1710 int rc = ascii_otel_sqlite_prepare(db, database_queries[DATABASE_QUERY_SetSessionTransportHost].sql, &statement);
1711 if (rc == SQLITE_OK)
1712 rc = sqlite3_bind_blob(statement, sqlite3_bind_parameter_index(statement, ":session_id"), session,
1713 SESSION_ID_SIZE, SQLITE_TRANSIENT);
1714 if (rc == SQLITE_OK)
1715 rc = sqlite3_bind_blob(statement, sqlite3_bind_parameter_index(statement, ":host_id"), host, SESSION_ID_SIZE,
1716 SQLITE_TRANSIENT);
1717 if (rc == SQLITE_OK)
1718 rc = sqlite3_step(statement);
1719 sqlite3_finalize(statement);
1720 if (rc != SQLITE_DONE)
1721 result = SET_ERRNO(ERROR_CONFIG, "Cannot bind transports to the new host");
1722 }
1723 if (result == ASCIICHAT_OK &&
1724 sqlite3_exec(db, database_queries[DATABASE_QUERY_CommitHostTransportRefresh].sql, NULL, NULL, NULL) != SQLITE_OK)
1725 result = SET_ERRNO(ERROR_CONFIG, "Cannot commit host transport update");
1726 if (result != ASCIICHAT_OK) {
1727 sqlite3_exec(db, database_queries[DATABASE_QUERY_RollbackHostTransportRefresh].sql, NULL, NULL, NULL);
1728 sqlite3_exec(db, database_queries[DATABASE_QUERY_CommitHostTransportRefresh].sql, NULL, NULL, NULL);
1729 }
1730 sqlite3_mutex_leave(mutex);
1731 return result;
1732}
asciichat_error_t database_session_update_host(sqlite3 *db, const uint8_t session_id[16], const uint8_t host_participant_id[16], const char *host_address, uint16_t host_port, uint8_t connection_type)
Update session host (discovery mode).
asciichat_error_t database_session_transport_set(sqlite3 *db, const uint8_t session_id[16], const void *metadata, size_t length)
@ DATABASE_QUERY_CommitHostTransportRefresh
@ DATABASE_QUERY_RollbackHostTransportRefresh
@ DATABASE_QUERY_SetSessionTransportHost
@ DATABASE_QUERY_BeginHostTransportRefresh

References ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_BeginHostTransportRefresh, DATABASE_QUERY_CommitHostTransportRefresh, DATABASE_QUERY_RollbackHostTransportRefresh, DATABASE_QUERY_SetSessionTransportHost, database_session_transport_set(), database_session_update_host(), ERROR_CONFIG, ERROR_INVALID_PARAM, SESSION_ID_SIZE, and SET_ERRNO.

◆ database_session_verify_key()

bool database_session_verify_key ( sqlite3 * db,
const char * session_string,
const uint8_t identity_pubkey[32] )

Check if a key is valid for a session (exists and not revoked).

Parameters
dbDatabase handle
session_stringSession string
identity_pubkeyEd25519 public key to check (32 bytes)
Returns
true if key is valid, false otherwise

Definition at line 1463 of file discovery/database.c.

1464 {
1465 ASCII_OTEL_SCOPE(DATABASE_VERIFY_KEY);
1466 if (!db || !session_string || !identity_pubkey) {
1467 return false;
1468 }
1469
1470 const char *sql = database_queries[DATABASE_QUERY_VerifySessionKey].sql;
1471
1472 sqlite3_stmt *stmt = NULL;
1473 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK) {
1474 return false;
1475 }
1476
1477 if ((sqlite3_bind_text(stmt, sqlite3_bind_parameter_index(stmt, ":session_string"), session_string, -1,
1478 SQLITE_STATIC) != SQLITE_OK) ||
1479 (sqlite3_bind_blob(stmt, sqlite3_bind_parameter_index(stmt, ":identity_pubkey"), identity_pubkey, 32,
1480 SQLITE_STATIC) != SQLITE_OK)) {
1481 sqlite3_finalize(stmt);
1482 log_error("Failed to bind SQLite query parameters");
1483 return false;
1484 }
1485
1486 bool valid = (sqlite3_step(stmt) == SQLITE_ROW);
1487 sqlite3_finalize(stmt);
1488
1489 return valid;
1490}
bool valid
Definition fft.c:27
@ DATABASE_QUERY_VerifySessionKey

References ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), DATABASE_QUERY_VerifySessionKey, ED25519_PUBLIC_KEY_SIZE, log_error, and valid.

◆ database_stats_session_count()

asciichat_error_t database_stats_session_count ( sqlite3 * db,
int64_t * count_out )

Get total number of active sessions.

Parameters
dbDatabase handle
count_outOutput: number of active sessions
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1532 of file discovery/database.c.

1532 {
1533 ASCII_OTEL_SCOPE(DATABASE_SESSION_COUNT);
1534 if (!db || !count_out) {
1535 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1536 }
1537
1538 const char *query = database_queries[DATABASE_QUERY_CountSessions].sql;
1539 sqlite3_stmt *stmt;
1540 int rc = ascii_otel_sqlite_prepare(db, query, &stmt);
1541 if (rc != SQLITE_OK) {
1542 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1543 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1544 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1545 }
1546
1547 time_t now = time(NULL);
1548 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":now"), (sqlite3_int64)now) != SQLITE_OK)) {
1549 sqlite3_finalize(stmt);
1550 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1551 }
1552
1553 int64_t count = 0;
1554 rc = sqlite3_step(stmt);
1555 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
1556 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
1557 if (rc == SQLITE_ROW) {
1558 count = sqlite3_column_int64(stmt, 0);
1559 } else if (rc != SQLITE_DONE) {
1560 sqlite3_finalize(stmt);
1561 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1562 }
1563
1564 sqlite3_finalize(stmt);
1565 *count_out = count;
1566
1567 log_debug("Active session count: %lld", (long long)count);
1568 return ASCIICHAT_OK;
1569}
@ DATABASE_QUERY_CountSessions

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_CountSessions, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.

◆ database_stats_snapshot()

asciichat_error_t database_stats_snapshot ( sqlite3 * db,
database_session_stats_t * rows,
size_t capacity,
size_t * row_count,
uint64_t * sessions,
uint64_t * participants )

Definition at line 1604 of file discovery/database.c.

1605 {
1606 ASCII_OTEL_SCOPE(DATABASE_STATS_SNAPSHOT);
1607 if (!db || (!rows && capacity) || !row_count || !sessions || !participants)
1608 return SET_ERRNO(ERROR_INVALID_PARAM, "Invalid database stats snapshot");
1609 *row_count = 0;
1610 *sessions = *participants = 0;
1611 sqlite3_stmt *stmt = NULL;
1612 const char *sql = database_queries[DATABASE_QUERY_GetStatsSnapshot].sql;
1613 if (ascii_otel_sqlite_prepare(db, sql, &stmt) != SQLITE_OK)
1614 return SET_ERRNO(ERROR_CONFIG, "Cannot query active session statistics");
1615 if ((sqlite3_bind_int64(stmt, sqlite3_bind_parameter_index(stmt, ":now"),
1616 (sqlite3_int64)(time_get_realtime_ns() / NS_PER_MS_INT)) != SQLITE_OK)) {
1617 sqlite3_finalize(stmt);
1618 return SET_ERRNO(ERROR_CONFIG, "Failed to bind SQLite query parameters");
1619 }
1620 int result;
1621 while ((result = sqlite3_step(stmt)) == SQLITE_ROW) {
1622 (*sessions)++;
1623 int count = sqlite3_column_int(stmt, 1);
1624 if (count > 0)
1625 *participants += (uint64_t)count;
1626 if (*row_count < capacity) {
1627 database_session_stats_t *row = &rows[(*row_count)++];
1628 snprintf(row->session_string, sizeof(row->session_string), "%s", (const char *)sqlite3_column_text(stmt, 0));
1629 row->participants = count > 0 ? (uint32_t)count : 0;
1630 row->host_ready = sqlite3_column_int(stmt, 2) != 0;
1631 row->migrating = sqlite3_column_int(stmt, 3) != 0;
1632 }
1633 }
1634 sqlite3_finalize(stmt);
1635 if (result != SQLITE_DONE)
1636 return SET_ERRNO(ERROR_CONFIG, "Active session statistics query failed");
1639 return ASCIICHAT_OK;
1640}
unsigned int uint32_t
Definition common.h:62
@ STATS_GAUGE_PARTICIPANTS_ACTIVE
void stats_gauge_set(stats_scope_t *scope, stats_gauge_id_t id, uint64_t value)
@ DATABASE_QUERY_GetStatsSnapshot
The useful part of database session stats is in participants, host_ready, migrating: together they de...
Definition database.h:285

References ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_GetStatsSnapshot, ERROR_CONFIG, ERROR_INVALID_PARAM, database_session_stats_t::host_ready, database_session_stats_t::migrating, NS_PER_MS_INT, database_session_stats_t::participants, database_session_stats_t::session_string, SET_ERRNO, STATS_GAUGE_PARTICIPANTS_ACTIVE, STATS_GAUGE_SESSIONS_ACTIVE, stats_gauge_set(), and stats_runtime_scope().

◆ database_stats_unique_clients()

asciichat_error_t database_stats_unique_clients ( sqlite3 * db,
int64_t * count_out )

Get total number of unique clients/participants.

Parameters
dbDatabase handle
count_outOutput: number of unique participants
Returns
ASCIICHAT_OK on success, error code otherwise

Definition at line 1571 of file discovery/database.c.

1571 {
1572 ASCII_OTEL_SCOPE(DATABASE_UNIQUE_CLIENTS);
1573 if (!db || !count_out) {
1574 return SET_ERRNO(ERROR_INVALID_PARAM, "db or count_out is NULL");
1575 }
1576
1577 const char *query = database_queries[DATABASE_QUERY_CountUniqueClients].sql;
1578 sqlite3_stmt *stmt;
1579 int rc = ascii_otel_sqlite_prepare(db, query, &stmt);
1580 if (rc != SQLITE_OK) {
1581 if (rc == SQLITE_BUSY || rc == SQLITE_LOCKED)
1582 ascii_otel_metric(ASCII_OTEL_METRIC_DB_BUSY, 1);
1583 return SET_ERRNO(ERROR_CONFIG, "Failed to prepare statement: %s", sqlite3_errmsg(db));
1584 }
1585
1586 int64_t count = 0;
1587 rc = sqlite3_step(stmt);
1588 if (rc == SQLITE_DONE && !sqlite3_stmt_readonly(stmt))
1589 ascii_otel_metric(ASCII_OTEL_METRIC_DB_ROWS, sqlite3_changes(db));
1590 if (rc == SQLITE_ROW) {
1591 count = sqlite3_column_int64(stmt, 0);
1592 } else if (rc != SQLITE_DONE) {
1593 sqlite3_finalize(stmt);
1594 return SET_ERRNO(ERROR_CONFIG, "Failed to execute query: %s", sqlite3_errmsg(db));
1595 }
1596
1597 sqlite3_finalize(stmt);
1598 *count_out = count;
1599
1600 log_debug("Unique clients count: %lld", (long long)count);
1601 return ASCIICHAT_OK;
1602}
@ DATABASE_QUERY_CountUniqueClients

References ascii_otel_metric(), ASCII_OTEL_SCOPE, ascii_otel_sqlite_prepare(), ASCIICHAT_OK, DATABASE_QUERY_CountUniqueClients, ERROR_CONFIG, ERROR_INVALID_PARAM, log_debug, and SET_ERRNO.