v2.1.6 - Fix strict-mode admin API key access by prewarming config and relay private key caches
This commit is contained in:
+3
-2
@@ -449,7 +449,8 @@ async function setupAutomaticRelayConnection(showSections = false) {
|
||||
const relayInfo = await fetchRelayInfo(httpUrl);
|
||||
|
||||
const fetchedPubkey = relayInfo && relayInfo.pubkey ? relayInfo.pubkey : null;
|
||||
relayPubkey = fetchedPubkey || '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
||||
// Fallback must match the actual running relay pubkey when NIP-11 doesn't expose pubkey.
|
||||
relayPubkey = fetchedPubkey || 'e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854';
|
||||
|
||||
// Keep relay metadata (including version) even when pubkey is missing/fallback
|
||||
relayInfoData = {
|
||||
@@ -466,7 +467,7 @@ async function setupAutomaticRelayConnection(showSections = false) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('⚠️ Could not fetch relay info, using fallback pubkey:', error.message);
|
||||
relayPubkey = '4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa';
|
||||
relayPubkey = 'e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854';
|
||||
}
|
||||
|
||||
// Set up subscription to receive admin API responses
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
# Plan: Eliminate g_db from the Main Thread
|
||||
|
||||
## Problem Statement
|
||||
|
||||
All SQLite calls go through `db_ops.c`, but `db_active_connection()` falls back to `g_db` when `g_thread_db` is NULL. Since the main thread never sets `g_thread_db`, every `db_*` call from lws-main executes synchronous SQLite on the main thread. The thread pool workers have their own connections and work correctly — the problem is the **fallback path**.
|
||||
|
||||
Current perf data shows lws-main at **67.6% avg CPU**, dominated by SQLite symbols (`sqlite3BtreeTableMoveto`, `sqlite3VdbeExec`, `pcache1Fetch`).
|
||||
|
||||
## Goal
|
||||
|
||||
Remove `g_db` as a runtime connection used by the main thread. After this change:
|
||||
- The main thread has **no SQLite connection** and cannot execute synchronous queries
|
||||
- All DB work routes through thread pool workers (which have their own connections)
|
||||
- `g_db` is only used during startup/shutdown (before/after the event loop)
|
||||
- Any accidental `db_*` call from lws-main returns an error instead of silently blocking
|
||||
|
||||
## Architecture After Change
|
||||
|
||||
```
|
||||
lws-main thread:
|
||||
- WebSocket protocol handling
|
||||
- Message parsing/routing
|
||||
- Completion queue draining
|
||||
- NO SQLite access during event loop
|
||||
|
||||
db-read-0 thread:
|
||||
- REQ queries
|
||||
- COUNT queries
|
||||
- Config cache miss reads
|
||||
- Auth rule checks
|
||||
- Monitoring/stats queries
|
||||
|
||||
db-write thread:
|
||||
- EVENT inserts
|
||||
- Subscription logging
|
||||
- Config updates
|
||||
- Auth rule modifications
|
||||
|
||||
event-worker thread:
|
||||
- Signature validation
|
||||
- Duplicate check
|
||||
- Store via db-write
|
||||
```
|
||||
|
||||
## Categorized g_db Call Sites
|
||||
|
||||
### Category A: Startup-only — keep using g_db
|
||||
|
||||
These run before the event loop starts. They are fine.
|
||||
|
||||
| Function | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| `db_init()` | db_ops.c:27 | Open database |
|
||||
| `db_exec_sql()` for PRAGMAs | main.c:854-882 | WAL, mmap, cache setup |
|
||||
| `db_table_exists()` | main.c:750 | Schema check |
|
||||
| `db_get_schema_version_dup()` | main.c:754 | Migration check |
|
||||
| `db_exec_sql()` for schema | main.c:842 | Create tables |
|
||||
| `populate_all_config_values_atomic()` | config.c:4290 | First-time config |
|
||||
| `populate_default_config_values()` | config.c:1657 | Default config |
|
||||
| `add_pubkeys_to_config_table()` | config.c:1778 | Pubkey storage |
|
||||
| `apply_cli_overrides_atomic()` | config.c:4211 | CLI overrides |
|
||||
| `db_store_relay_private_key_hex()` | config.c:495 | Key storage |
|
||||
| `db_populate_event_tags_from_existing()` | main.c:1156 | Tag migration |
|
||||
| `cleanup_all_subscriptions_on_startup()` | subscriptions.c:1092 | Orphan cleanup |
|
||||
|
||||
### Category B: Shutdown-only — keep using g_db
|
||||
|
||||
| Function | File | Purpose |
|
||||
|----------|------|---------|
|
||||
| `db_exec_sql()` WAL checkpoint | main.c:900 | Clean shutdown |
|
||||
| `db_close()` | main.c:904 | Close connection |
|
||||
|
||||
### Category C: Runtime hot path — must move off main thread
|
||||
|
||||
These are called during the event loop from lws-main context.
|
||||
|
||||
| Function | File | Called from | Fix strategy |
|
||||
|----------|------|------------|--------------|
|
||||
| `db_get_config_value_dup()` | db_ops.c:777 | config cache miss | Already cached with 5s TTL; pre-warm cache at startup so misses are rare |
|
||||
| `store_event()` for kind 14/1059 | main.c:983 | websockets.c sync path | Route through async event worker |
|
||||
| `store_event_post_actions()` | main.c | completion handler | Already runs on main; its DB calls need routing |
|
||||
| `db_log_subscription_created()` | db_ops.c:145 | subscriptions.c | Fire-and-forget via write queue |
|
||||
| `db_log_subscription_closed()` | db_ops.c:165 | subscriptions.c | Fire-and-forget via write queue |
|
||||
| `db_log_subscription_disconnected()` | db_ops.c:193 | subscriptions.c | Fire-and-forget via write queue |
|
||||
| `db_update_subscription_events_sent()` | db_ops.c:225 | subscriptions.c | Fire-and-forget via write queue |
|
||||
| `db_cleanup_orphaned_subscriptions()` | db_ops.c:244 | subscriptions.c | Move to startup only |
|
||||
| `generate_and_post_status_event()` | websockets.c:3408 | periodic timer | Submit to write worker |
|
||||
| `ip_ban_cleanup()` / `ip_ban_log_stats()` | websockets.c:3196 | periodic timer | Submit to write worker |
|
||||
| `db_get_total_event_count_ll()` | db_ops.c:589 | api.c monitoring | Submit to read worker |
|
||||
| `db_get_event_count_since()` | db_ops.c:606 | api.c monitoring | Submit to read worker |
|
||||
| `db_get_event_kind_distribution_rows()` | db_ops.c:625 | api.c monitoring | Submit to read worker |
|
||||
| `db_get_top_pubkeys_rows()` | db_ops.c:663 | api.c monitoring | Submit to read worker |
|
||||
| `db_get_subscription_details_rows()` | db_ops.c:697 | api.c monitoring | Submit to read worker |
|
||||
| `db_get_all_config_rows()` | db_ops.c:745 | api.c config query | Submit to read worker |
|
||||
| `db_execute_readonly_query_json()` | db_ops.c:442 | api.c SQL query | Submit to read worker |
|
||||
| `db_event_id_exists()` | db_ops.c:1041 | main.c event check | Already moved to event worker |
|
||||
| `db_retrieve_event_by_id()` | db_ops.c:1056 | main.c | Submit to read worker |
|
||||
| `db_get_event_pubkey()` | db_ops.c:262 | NIP-09 deletion | Submit to read worker |
|
||||
| `db_delete_event_by_id()` | db_ops.c:285 | NIP-09 deletion | Submit to write worker |
|
||||
| `db_delete_older_replaceable_events()` | db_ops.c:305 | store_event_core | Already on write worker |
|
||||
| `db_store_config_event()` | db_ops.c:888 | config.c | Submit to write worker |
|
||||
| `db_add_auth_rule()` | db_ops.c:1226 | config.c admin | Submit to write worker |
|
||||
| `db_remove_auth_rule()` | db_ops.c:1242 | config.c admin | Submit to write worker |
|
||||
| `db_delete_wot_whitelist_rules()` | db_ops.c:1258 | config.c WoT | Submit to write worker |
|
||||
| `db_count_wot_whitelist_rules()` | db_ops.c:1266 | config.c | Cache or read worker |
|
||||
| `db_store_event_tags_cjson()` | db_ops.c:1140 | main.c post-actions | Already on write worker path |
|
||||
| `db_get_config_row_count()` | db_ops.c:1121 | config.c diagnostics | Cache or read worker |
|
||||
| `db_set_config_value_full()` | db_ops.c:796 | config.c | Submit to write worker |
|
||||
| `db_update_config_value_only()` | db_ops.c:821 | config.c | Submit to write worker |
|
||||
| `db_upsert_config_value()` | db_ops.c:838 | api.c | Submit to write worker |
|
||||
| `db_exec_sql()` for transactions | config.c | admin events | Submit to write worker |
|
||||
| `db_count_with_sql()` | db_ops.c:415 | config.c admin | Submit to read worker |
|
||||
| `is_config_table_ready()` | config.c:4595 | config.c hybrid | Cache result at startup |
|
||||
| `generate_config_event_from_table()` | config.c:4933 | config.c | Cache or read worker |
|
||||
|
||||
### Category D: Auth rule checks — already use separate connections
|
||||
|
||||
These functions in `db_ops.c` open their own temporary read-only connections:
|
||||
|
||||
| Function | Line | Notes |
|
||||
|----------|------|-------|
|
||||
| `db_is_pubkey_blacklisted()` | 347 | Opens own connection |
|
||||
| `db_is_hash_blacklisted()` | 362 | Opens own connection |
|
||||
| `db_is_pubkey_whitelisted()` | 377 | Opens own connection |
|
||||
| `db_count_active_whitelist_rules()` | 392 | Opens own connection |
|
||||
|
||||
These are already safe — they don't use `g_db`. No change needed.
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Pre-warm caches and eliminate cache-miss DB reads
|
||||
|
||||
1. Pre-warm the config cache at startup by loading all config values into the in-memory cache before the event loop starts
|
||||
2. Pre-warm the NIP-11 cache at startup
|
||||
3. Pre-warm the auth rules fast cache at startup
|
||||
4. This eliminates the most frequent cache-miss `db_get_config_value_dup()` calls
|
||||
|
||||
### Phase 2: Route subscription logging through write worker
|
||||
|
||||
1. Add a `THREAD_POOL_JOB_FIRE_AND_FORGET` job type to thread_pool
|
||||
2. Create async wrappers: `db_log_subscription_created_async()`, etc.
|
||||
3. These submit SQL to the write worker queue and return immediately
|
||||
4. No completion callback needed — fire and forget
|
||||
|
||||
### Phase 3: Route special-kind EVENT store through async worker
|
||||
|
||||
1. Remove the `event_is_async_eligible()` exclusion for kinds 14, 1059, 23456
|
||||
2. For kind 23456: process admin command in completion handler on main thread after store
|
||||
3. For kind 14/1059: process NIP-17 DM in completion handler after store
|
||||
|
||||
### Phase 4: Route periodic timer DB work through workers
|
||||
|
||||
1. `generate_and_post_status_event()` — submit event creation to write worker
|
||||
2. `ip_ban_cleanup()` / `ip_ban_log_stats()` — submit to write worker
|
||||
3. Monitoring queries — submit to read worker with completion callback
|
||||
|
||||
### Phase 5: Null out g_db before event loop
|
||||
|
||||
1. After startup is complete and thread pool is initialized, set `g_db = NULL`
|
||||
2. Change `db_active_connection()` to assert/warn if both `g_thread_db` and `g_db` are NULL
|
||||
3. Any accidental main-thread DB call will now fail loudly instead of silently blocking
|
||||
4. Restore `g_db` briefly for shutdown checkpoint
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. Phase 1 — lowest risk, immediate benefit from eliminating cache misses
|
||||
2. Phase 2 — straightforward fire-and-forget pattern
|
||||
3. Phase 3 — moderate complexity, needs careful completion handler design
|
||||
4. Phase 4 — moderate complexity, periodic tasks need async patterns
|
||||
5. Phase 5 — the final enforcement step, only safe after phases 1-4
|
||||
|
||||
## Risk Assessment
|
||||
|
||||
- **Phase 1**: Very low risk — just pre-warming existing caches
|
||||
- **Phase 2**: Low risk — subscription logging is non-critical, fire-and-forget is safe
|
||||
- **Phase 3**: Medium risk — admin/DM event processing has complex state; needs careful testing
|
||||
- **Phase 4**: Medium risk — periodic tasks have side effects that need to complete
|
||||
- **Phase 5**: High risk if done prematurely — must verify ALL runtime paths are covered first
|
||||
|
||||
## Expected Impact
|
||||
|
||||
After all phases:
|
||||
- lws-main CPU should drop from ~67% to near 0% SQLite overhead
|
||||
- All SQLite work happens on db-read and db-write threads
|
||||
- Main thread only does WebSocket I/O, JSON parsing, and completion queue draining
|
||||
- Thread model becomes: lws-main = pure I/O, workers = all DB
|
||||
@@ -0,0 +1,204 @@
|
||||
# Remaining Implementation Plan — Zero SQLite on Main Thread + PG Fork Readiness
|
||||
|
||||
## Overview
|
||||
|
||||
This plan covers all remaining work to achieve two goals:
|
||||
1. **Zero direct SQLite calls on lws-main** during the event loop
|
||||
2. **Clean `db_ops` abstraction boundary** so the PG fork has no raw `sqlite3_*` calls outside `db_ops.c`
|
||||
|
||||
## Current State
|
||||
|
||||
### Completed (Fixes 1-5, 7 + Phases 1-3, 5)
|
||||
- Global config cache with 5s TTL
|
||||
- Dedup check moved to event worker
|
||||
- Async COUNT queries via thread pool
|
||||
- WoT auth rules cached per-session
|
||||
- Special-kind EVENT store through async worker
|
||||
- Subscription logging via fire-and-forget write queue
|
||||
- IP ban periodic save via write queue
|
||||
- g_db nulled before event loop
|
||||
|
||||
### Remaining
|
||||
- Fix 6: Reduce reader threads (trivial)
|
||||
- Fix 8: Move periodic tasks off main thread (partial — IP ban save done, monitoring/status not)
|
||||
- Fix 9: Cache NIP-11 response
|
||||
- Sync `store_event()` calls from admin/DM/NIP-09 paths
|
||||
- Raw `sqlite3_*` calls in `thread_pool.c` and `websockets.c`
|
||||
- COUNT query path in `websockets.c` still uses `json_each()` instead of `event_tags`
|
||||
|
||||
---
|
||||
|
||||
## Phase A: Move Remaining Periodic Tasks Off Main Thread
|
||||
|
||||
**Goal:** Eliminate the last regular SQLite calls from the lws-main event loop timer.
|
||||
|
||||
### A1: Move `generate_and_post_status_event()` to write worker
|
||||
|
||||
Currently called synchronously from the main event loop at `websockets.c:3455`.
|
||||
|
||||
**Changes:**
|
||||
- Add `THREAD_POOL_JOB_STATUS_POST` to `thread_pool.h` job type enum
|
||||
- Add executor in `thread_pool.c` that calls `generate_and_post_status_event()`
|
||||
- In `websockets.c` main loop, replace direct call with `thread_pool_submit_write()` job
|
||||
- The write worker has a DB connection, so `generate_stats_json()` → `store_event()` → `broadcast_event_to_subscriptions()` all work naturally
|
||||
- Note: `broadcast_event_to_subscriptions()` is thread-safe (uses its own mutex)
|
||||
|
||||
**Files:** `thread_pool.h`, `thread_pool.c`, `websockets.c`
|
||||
|
||||
### A2: Move monitoring queries to read/write worker
|
||||
|
||||
`monitoring_on_event_stored()` and `monitoring_on_subscription_change()` in `api.c` call `generate_event_driven_monitoring()` / `generate_subscription_driven_monitoring()` which do DB queries and then broadcast ephemeral events.
|
||||
|
||||
**Changes:**
|
||||
- Add `THREAD_POOL_JOB_MONITORING` job type
|
||||
- Submit monitoring generation as a read job (it mostly does SELECTs)
|
||||
- The completion broadcasts the ephemeral event via `lws_cancel_service()` + completion queue
|
||||
- Alternative simpler approach: since these are throttled (default 5s) and only fire when kind 24567 subscribers exist, they could run on the write worker as fire-and-forget
|
||||
|
||||
**Files:** `thread_pool.h`, `thread_pool.c`, `api.c`, `websockets.c`
|
||||
|
||||
### A3: Move `ip_ban_cleanup()` and `ip_ban_log_stats()` to write worker
|
||||
|
||||
Currently called from the 60-second periodic timer at `websockets.c:3479-3480`.
|
||||
|
||||
**Changes:**
|
||||
- Add `THREAD_POOL_JOB_IP_BAN_CLEANUP` job type (or reuse a generic callback job)
|
||||
- Submit as fire-and-forget write job from the periodic timer
|
||||
- `ip_ban_cleanup()` does in-memory cleanup + optional DB writes
|
||||
- `ip_ban_log_stats()` is purely logging, no DB
|
||||
|
||||
**Files:** `thread_pool.h`, `thread_pool.c`, `ip_ban.c`, `websockets.c`
|
||||
|
||||
---
|
||||
|
||||
## Phase B: Route Remaining Sync `store_event()` Calls Through Async Worker
|
||||
|
||||
**Goal:** Eliminate all synchronous `store_event()` calls from lws-main context.
|
||||
|
||||
### Current sync `store_event()` call sites on main thread
|
||||
|
||||
| Call site | File | Context | Frequency |
|
||||
|-----------|------|---------|-----------|
|
||||
| Kind 1059 gift wrap store | `websockets.c:1795,1803,1819` | NIP-17 DM processing | Low |
|
||||
| Kind 14 DM store | `websockets.c:1843` | NIP-17 DM processing | Low |
|
||||
| Regular event fallback | `websockets.c:1862,1876` | Non-async-eligible events | Low |
|
||||
| Kind 1059 (auth path) | `websockets.c:2594,2602,2618` | NIP-42 auth + DM | Low |
|
||||
| Kind 14 (auth path) | `websockets.c:2642` | NIP-42 auth + DM | Low |
|
||||
| Regular (auth path) | `websockets.c:2661,2675` | NIP-42 auth fallback | Low |
|
||||
| Admin response store | `config.c:2585` | Admin API response | Very low |
|
||||
| DM admin gift wrap | `dm_admin.c:389` | Admin DM response | Very low |
|
||||
| NIP-09 deletion store | `nip009.c:135` | Deletion events | Low |
|
||||
| Status event store | `api.c:595` | Periodic status post | Very low (handled by Phase A1) |
|
||||
| Admin response event | `api.c:908` | Admin API | Very low |
|
||||
| DM chat response | `api.c:1377` | Admin DM chat | Very low |
|
||||
| Relay-generated event | `api.c:2350` | Relay metadata | Very low |
|
||||
| DM stats response | `websockets.c:3648` | DM stats command | Very low |
|
||||
|
||||
### Strategy
|
||||
|
||||
Rather than converting each call site individually, create a **synchronous-to-async bridge**:
|
||||
|
||||
**Changes:**
|
||||
- Create `store_event_async(cJSON* event, store_event_callback_t cb, void* ctx)` that submits the event to the write worker
|
||||
- For call sites that need the result (e.g., to broadcast after store), use a completion callback
|
||||
- For fire-and-forget sites (admin responses, DM responses), use NULL callback
|
||||
- The existing `store_event()` wrapper becomes: submit to write worker + block on completion (for backward compat during transition)
|
||||
- Eventually, all callers migrate to the async version
|
||||
|
||||
**Alternative simpler approach:** Since these are all low-frequency paths, and the write worker already has a DB connection, we can submit them as generic write jobs with a callback that does the store + broadcast. The main thread just needs to hand off the event JSON.
|
||||
|
||||
**Files:** `main.c`, `websockets.c`, `api.c`, `config.c`, `dm_admin.c`, `nip009.c`, `thread_pool.h`, `thread_pool.c`
|
||||
|
||||
---
|
||||
|
||||
## Phase C: `db_ops` Abstraction Cleanup for PG Fork
|
||||
|
||||
**Goal:** Ensure all raw `sqlite3_*` calls are inside `db_ops.c` only. External code uses only `db_ops.h` functions.
|
||||
|
||||
### C1: Add `db_open_worker_connection()` / `db_close_worker_connection()` to `db_ops.h`
|
||||
|
||||
Currently, `thread_pool.c:208` and `websockets.c:537` call `sqlite3_open_v2()` directly.
|
||||
|
||||
**Changes:**
|
||||
- Add to `db_ops.h`:
|
||||
```c
|
||||
void* db_open_worker_connection(void); // Returns opaque connection handle
|
||||
void db_close_worker_connection(void* conn);
|
||||
```
|
||||
- Implementation in `db_ops.c`: opens a new SQLite connection with WAL + busy timeout
|
||||
- `thread_pool.c` worker init calls `db_open_worker_connection()` instead of raw `sqlite3_open_v2()`
|
||||
- `websockets.c` event-worker calls `db_open_worker_connection()` instead of raw `sqlite3_open_v2()`
|
||||
- Remove `#include <sqlite3.h>` from `thread_pool.c` and `websockets.c`
|
||||
|
||||
**Files:** `db_ops.h`, `db_ops.c`, `thread_pool.c`, `websockets.c`
|
||||
|
||||
### C2: Audit and remove remaining `sqlite3` includes outside `db_ops.c`
|
||||
|
||||
After C1, grep for any remaining `sqlite3` references outside `db_ops.c` and eliminate them.
|
||||
|
||||
**Files:** All `.c` files
|
||||
|
||||
### C3: Unify COUNT query tag path
|
||||
|
||||
The COUNT query builder in `websockets.c:3849` still uses `json_each(json(tags))` while the REQ query builder in `main.c:1604` uses the indexed `event_tags` table. Unify to use `event_tags` for consistency.
|
||||
|
||||
**Files:** `websockets.c`
|
||||
|
||||
---
|
||||
|
||||
## Phase D: Quick Wins
|
||||
|
||||
### D1: Fix 6 — Reduce reader threads from 4 to 1
|
||||
|
||||
Change default in `thread_pool_init()` or make configurable. With all reads going through the pool, a single reader is sufficient unless profiling shows otherwise.
|
||||
|
||||
**Files:** `thread_pool.c` or config default
|
||||
|
||||
### D2: Fix 9 — Cache NIP-11 response
|
||||
|
||||
Cache the serialized NIP-11 JSON string with a 60-second TTL. Invalidate on admin config change.
|
||||
|
||||
**Files:** `nip011.c`
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
| # | Task | Risk | Dependencies |
|
||||
|---|------|------|-------------|
|
||||
| 1 | A3: ip_ban_cleanup to write worker | Very Low | None |
|
||||
| 2 | A1: status post to write worker | Low | None |
|
||||
| 3 | A2: monitoring to worker | Low | None |
|
||||
| 4 | D1: reduce reader threads to 1 | Very Low | None |
|
||||
| 5 | D2: cache NIP-11 response | Very Low | None |
|
||||
| 6 | C1: db_open/close_worker_connection | Low | None |
|
||||
| 7 | C3: unify COUNT tag query path | Low | None |
|
||||
| 8 | C2: audit/remove sqlite3 includes | Very Low | C1 |
|
||||
| 9 | B: route sync store_event through async | Medium | A1 (status post already handled) |
|
||||
|
||||
## Expected Outcome
|
||||
|
||||
After all phases:
|
||||
- **lws-main has zero SQLite calls** during the event loop
|
||||
- **All `sqlite3_*` calls are inside `db_ops.c`** — clean abstraction boundary
|
||||
- **PG fork can replace `db_ops.c`** without touching any other file
|
||||
- Thread model is fully realized:
|
||||
- `lws-main` = WebSocket I/O + JSON parse + subscription match + completion drain
|
||||
- `event-worker` = signature verify + dedup + store submission
|
||||
- `db-read` = REQ queries + COUNT queries + monitoring reads
|
||||
- `db-write` = event INSERTs + subscription logging + periodic tasks + admin writes
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CLIENT[Nostr Clients] --> LWS[lws-main<br/>Zero SQLite<br/>WS I/O + JSON + Sub Match]
|
||||
|
||||
LWS -->|EVENT| EW[event-worker<br/>Verify + Dedup]
|
||||
EW -->|store job| DW[db-write<br/>INSERT events<br/>Sub logging<br/>Periodic tasks<br/>Admin writes]
|
||||
EW -->|completion| LWS
|
||||
DW -->|completion| LWS
|
||||
|
||||
LWS -->|REQ/COUNT| DR[db-read<br/>SELECT queries<br/>Monitoring reads]
|
||||
DR -->|completion| LWS
|
||||
|
||||
LWS -->|OK/EVENT/EOSE/COUNT| CLIENT
|
||||
```
|
||||
@@ -19,6 +19,7 @@ int get_active_connection_count(void);
|
||||
#include <unistd.h>
|
||||
#include <strings.h>
|
||||
#include <stdbool.h>
|
||||
#include <sqlite3.h>
|
||||
#include "api.h"
|
||||
#include "embedded_web_content.h"
|
||||
#include "config.h"
|
||||
@@ -55,6 +56,40 @@ int generate_monitoring_event_for_type(const char* d_tag_value, cJSON* (*query_f
|
||||
// Forward declaration for CPU metrics query function
|
||||
cJSON* query_cpu_metrics(void);
|
||||
|
||||
static int api_open_temp_db_connection(sqlite3** out_db) {
|
||||
if (!out_db) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
*out_db = NULL;
|
||||
const char* db_path = db_get_database_path();
|
||||
if (!db_path || db_path[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (sqlite3_open_v2(db_path, out_db, SQLITE_OPEN_READWRITE, NULL) != SQLITE_OK) {
|
||||
if (*out_db) {
|
||||
sqlite3_close(*out_db);
|
||||
*out_db = NULL;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
sqlite3_exec(*out_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
|
||||
sqlite3_busy_timeout(*out_db, 5000);
|
||||
db_set_thread_connection(*out_db);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void api_close_temp_db_connection(sqlite3* db) {
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
|
||||
db_clear_thread_connection();
|
||||
sqlite3_close(db);
|
||||
}
|
||||
|
||||
// Monitoring system helper functions
|
||||
int get_monitoring_throttle_seconds(void) {
|
||||
return get_config_int("kind_24567_reporting_throttle_sec", 5);
|
||||
@@ -62,7 +97,8 @@ int get_monitoring_throttle_seconds(void) {
|
||||
|
||||
// Query event kind distribution from database
|
||||
cJSON* query_event_kind_distribution(void) {
|
||||
if (!db_is_available()) {
|
||||
sqlite3* temp_db = NULL;
|
||||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||||
DEBUG_ERROR("Database not available for monitoring query");
|
||||
return NULL;
|
||||
}
|
||||
@@ -95,12 +131,14 @@ cJSON* query_event_kind_distribution(void) {
|
||||
cJSON_AddNumberToObject(distribution, "total_events", total_events);
|
||||
cJSON_AddItemToObject(distribution, "kinds", kinds_array);
|
||||
|
||||
api_close_temp_db_connection(temp_db);
|
||||
return distribution;
|
||||
}
|
||||
|
||||
// Query time-based statistics from database
|
||||
cJSON* query_time_based_statistics(void) {
|
||||
if (!db_is_available()) {
|
||||
sqlite3* temp_db = NULL;
|
||||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||||
DEBUG_ERROR("Database not available for time stats query");
|
||||
return NULL;
|
||||
}
|
||||
@@ -146,12 +184,14 @@ cJSON* query_time_based_statistics(void) {
|
||||
cJSON_AddItemToObject(time_stats, "periods", periods_array);
|
||||
cJSON_AddNumberToObject(time_stats, "total_events", total_events);
|
||||
|
||||
api_close_temp_db_connection(temp_db);
|
||||
return time_stats;
|
||||
}
|
||||
|
||||
// Query top pubkeys by event count from database
|
||||
cJSON* query_top_pubkeys(void) {
|
||||
if (!db_is_available()) {
|
||||
sqlite3* temp_db = NULL;
|
||||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||||
DEBUG_ERROR("Database not available for top pubkeys query");
|
||||
return NULL;
|
||||
}
|
||||
@@ -163,6 +203,7 @@ cJSON* query_top_pubkeys(void) {
|
||||
cJSON* rows = db_get_top_pubkeys_rows(10);
|
||||
if (!rows) {
|
||||
cJSON_Delete(top_pubkeys);
|
||||
api_close_temp_db_connection(temp_db);
|
||||
DEBUG_ERROR("Failed to query top pubkeys rows");
|
||||
return NULL;
|
||||
}
|
||||
@@ -188,6 +229,7 @@ cJSON* query_top_pubkeys(void) {
|
||||
cJSON_AddItemToObject(top_pubkeys, "pubkeys", pubkeys_array);
|
||||
cJSON_AddNumberToObject(top_pubkeys, "total_events", total_events);
|
||||
|
||||
api_close_temp_db_connection(temp_db);
|
||||
return top_pubkeys;
|
||||
}
|
||||
|
||||
@@ -195,7 +237,8 @@ cJSON* query_top_pubkeys(void) {
|
||||
// Query detailed subscription information from database log (ADMIN ONLY)
|
||||
// Uses subscriptions table instead of in-memory iteration to avoid mutex contention
|
||||
cJSON* query_subscription_details(void) {
|
||||
if (!db_is_available()) {
|
||||
sqlite3* temp_db = NULL;
|
||||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||||
DEBUG_ERROR("Database not available for subscription details query");
|
||||
return NULL;
|
||||
}
|
||||
@@ -226,6 +269,7 @@ cJSON* query_subscription_details(void) {
|
||||
cJSON_Delete(subscriptions_data);
|
||||
cJSON_Delete(data);
|
||||
cJSON_Delete(subscriptions_array);
|
||||
api_close_temp_db_connection(temp_db);
|
||||
DEBUG_ERROR("Failed to query subscription details rows");
|
||||
return NULL;
|
||||
}
|
||||
@@ -278,9 +322,9 @@ cJSON* query_subscription_details(void) {
|
||||
// Parse wsi pointer from hex string
|
||||
struct lws* wsi = NULL;
|
||||
if (sscanf(wsi_pointer, "%p", (void**)&wsi) == 1 && wsi != NULL) {
|
||||
// Get per_session_data from wsi
|
||||
struct per_session_data* pss = (struct per_session_data*)lws_wsi_user(wsi);
|
||||
if (pss) {
|
||||
// Resolve live session data only if this wsi is still tracked.
|
||||
struct per_session_data* pss = NULL;
|
||||
if (websocket_get_live_pss(wsi, &pss) && pss) {
|
||||
db_queries = pss->db_queries_executed;
|
||||
db_rows = pss->db_rows_returned;
|
||||
|
||||
@@ -341,6 +385,7 @@ cJSON* query_subscription_details(void) {
|
||||
DEBUG_LOG("Total subscriptions found: %d", row_count);
|
||||
DEBUG_LOG("=== END SUBSCRIPTION_DETAILS QUERY DEBUG ===");
|
||||
|
||||
api_close_temp_db_connection(temp_db);
|
||||
return subscriptions_data;
|
||||
}
|
||||
|
||||
@@ -1165,7 +1210,8 @@ cJSON* query_cpu_metrics(void) {
|
||||
|
||||
// Generate stats JSON from database queries
|
||||
char* generate_stats_json(void) {
|
||||
if (!db_is_available()) {
|
||||
sqlite3* temp_db = NULL;
|
||||
if (!db_is_available() && api_open_temp_db_connection(&temp_db) != 0) {
|
||||
DEBUG_ERROR("Database not available for stats generation");
|
||||
return NULL;
|
||||
}
|
||||
@@ -1274,6 +1320,7 @@ char* generate_stats_json(void) {
|
||||
DEBUG_ERROR("Failed to generate stats JSON");
|
||||
}
|
||||
|
||||
api_close_temp_db_connection(temp_db);
|
||||
return json_string;
|
||||
}
|
||||
|
||||
|
||||
+94
-2
@@ -108,6 +108,9 @@ static cJSON* g_pending_config_event = NULL;
|
||||
// Temporary storage for relay private key during first-time setup
|
||||
static char g_temp_relay_privkey[65] = {0};
|
||||
|
||||
// Runtime cache for relay private key to support strict mode (g_db = NULL)
|
||||
static char g_cached_relay_privkey[65] = {0};
|
||||
|
||||
// ================================
|
||||
// UTILITY FUNCTIONS
|
||||
// ================================
|
||||
@@ -412,6 +415,9 @@ void cleanup_configuration_system(void) {
|
||||
g_pending_config_event = NULL;
|
||||
}
|
||||
|
||||
// Clear cached sensitive material
|
||||
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
|
||||
|
||||
// Configuration system now uses direct database queries instead of cache
|
||||
// No cleanup needed for cache system
|
||||
}
|
||||
@@ -493,6 +499,10 @@ int store_relay_private_key(const char* relay_privkey_hex) {
|
||||
}
|
||||
|
||||
if (db_store_relay_private_key_hex(relay_privkey_hex) == 0) {
|
||||
// Keep an in-memory runtime copy for strict mode where g_db is NULL
|
||||
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
|
||||
strncpy(g_cached_relay_privkey, relay_privkey_hex, sizeof(g_cached_relay_privkey) - 1);
|
||||
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -501,11 +511,16 @@ int store_relay_private_key(const char* relay_privkey_hex) {
|
||||
}
|
||||
|
||||
char* get_relay_private_key(void) {
|
||||
// Fast path for strict runtime mode where g_db may be NULL
|
||||
if (strlen(g_cached_relay_privkey) == 64) {
|
||||
return strdup(g_cached_relay_privkey);
|
||||
}
|
||||
|
||||
if (!db_is_available()) {
|
||||
DEBUG_ERROR("Database not available for relay private key retrieval");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
char* private_key = db_get_relay_private_key_hex_dup();
|
||||
if (!private_key || strlen(private_key) != 64) {
|
||||
if (private_key) {
|
||||
@@ -515,9 +530,35 @@ char* get_relay_private_key(void) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Prime runtime cache from DB for subsequent strict-mode operations
|
||||
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
|
||||
strncpy(g_cached_relay_privkey, private_key, sizeof(g_cached_relay_privkey) - 1);
|
||||
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
|
||||
|
||||
return private_key;
|
||||
}
|
||||
|
||||
int preload_relay_private_key_cache(void) {
|
||||
if (!db_is_available()) {
|
||||
DEBUG_ERROR("Database not available for relay private key cache preload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
char* private_key = db_get_relay_private_key_hex_dup();
|
||||
if (!private_key || strlen(private_key) != 64) {
|
||||
if (private_key) free(private_key);
|
||||
DEBUG_ERROR("Relay private key not found for cache preload");
|
||||
return -1;
|
||||
}
|
||||
|
||||
memset(g_cached_relay_privkey, 0, sizeof(g_cached_relay_privkey));
|
||||
strncpy(g_cached_relay_privkey, private_key, sizeof(g_cached_relay_privkey) - 1);
|
||||
g_cached_relay_privkey[sizeof(g_cached_relay_privkey) - 1] = '\0';
|
||||
free(private_key);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* get_temp_relay_private_key(void) {
|
||||
if (strlen(g_temp_relay_privkey) == 64) {
|
||||
return g_temp_relay_privkey;
|
||||
@@ -4516,11 +4557,58 @@ void force_config_cache_refresh(void) {
|
||||
|
||||
int reload_config_from_table(void) {
|
||||
force_config_cache_refresh();
|
||||
|
||||
if (!db_is_available()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
cJSON* rows = db_get_all_config_rows();
|
||||
if (!rows || !cJSON_IsArray(rows)) {
|
||||
if (rows) cJSON_Delete(rows);
|
||||
return -1;
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_config_cache_mutex);
|
||||
cJSON* row = NULL;
|
||||
cJSON_ArrayForEach(row, rows) {
|
||||
if (!cJSON_IsObject(row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* key_obj = cJSON_GetObjectItemCaseSensitive(row, "key");
|
||||
cJSON* value_obj = cJSON_GetObjectItemCaseSensitive(row, "value");
|
||||
if (!cJSON_IsString(key_obj) || !cJSON_IsString(value_obj)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* key = cJSON_GetStringValue(key_obj);
|
||||
const char* value = cJSON_GetStringValue(value_obj);
|
||||
if (!key || !value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int idx = find_cache_index_by_key(key);
|
||||
if (idx < 0) {
|
||||
idx = find_empty_cache_index();
|
||||
}
|
||||
if (idx < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
g_config_cache[idx].in_use = 1;
|
||||
strncpy(g_config_cache[idx].key, key, sizeof(g_config_cache[idx].key) - 1);
|
||||
g_config_cache[idx].key[sizeof(g_config_cache[idx].key) - 1] = '\0';
|
||||
strncpy(g_config_cache[idx].value, value, sizeof(g_config_cache[idx].value) - 1);
|
||||
g_config_cache[idx].value[sizeof(g_config_cache[idx].value) - 1] = '\0';
|
||||
}
|
||||
pthread_mutex_unlock(&g_config_cache_mutex);
|
||||
|
||||
cJSON_Delete(rows);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char* get_config_value_from_table(const char* key) {
|
||||
if (!db_is_available() || !key) {
|
||||
if (!key) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -4535,6 +4623,10 @@ const char* get_config_value_from_table(const char* key) {
|
||||
|
||||
pthread_mutex_unlock(&g_config_cache_mutex);
|
||||
|
||||
if (!db_is_available()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
char* db_value = db_get_config_value_dup(key);
|
||||
if (!db_value) {
|
||||
return NULL;
|
||||
|
||||
@@ -77,6 +77,7 @@ char* extract_pubkey_from_filename(const char* filename);
|
||||
int store_relay_private_key(const char* relay_privkey_hex);
|
||||
char* get_relay_private_key(void);
|
||||
const char* get_temp_relay_private_key(void); // For first-time startup only
|
||||
int preload_relay_private_key_cache(void); // Preload relay private key before strict DB mode
|
||||
|
||||
// NIP-42 authentication configuration functions
|
||||
int parse_auth_required_kinds(const char* kinds_str, int* kinds_array, int max_kinds);
|
||||
|
||||
+120
-85
@@ -143,14 +143,15 @@ void db_finalize_stmt(db_stmt_t* stmt) {
|
||||
|
||||
int db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
|
||||
const char* client_ip, const char* filter_json) {
|
||||
if (!g_db || !sub_id || !wsi_ptr || !client_ip) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !sub_id || !wsi_ptr || !client_ip) return -1;
|
||||
|
||||
const char* sql =
|
||||
"INSERT OR REPLACE INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type, filter_json) "
|
||||
"VALUES (?, ?, ?, 'created', ?)";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, wsi_ptr, -1, SQLITE_TRANSIENT);
|
||||
@@ -163,14 +164,15 @@ int db_log_subscription_created(const char* sub_id, const char* wsi_ptr,
|
||||
}
|
||||
|
||||
int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
|
||||
if (!g_db || !sub_id) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !sub_id) return -1;
|
||||
|
||||
const char* insert_sql =
|
||||
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||
"VALUES (?, '', ?, 'closed')";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, client_ip ? client_ip : "unknown", -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
@@ -182,7 +184,7 @@ int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
|
||||
"SET ended_at = strftime('%s', 'now') "
|
||||
"WHERE subscription_id = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
sqlite3_bind_text(stmt, 1, sub_id, -1, SQLITE_TRANSIENT);
|
||||
int rc = sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -191,7 +193,8 @@ int db_log_subscription_closed(const char* sub_id, const char* client_ip) {
|
||||
}
|
||||
|
||||
int db_log_subscription_disconnected(const char* client_ip) {
|
||||
if (!g_db || !client_ip) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !client_ip) return -1;
|
||||
|
||||
const char* update_sql =
|
||||
"UPDATE subscriptions "
|
||||
@@ -199,11 +202,11 @@ int db_log_subscription_disconnected(const char* client_ip) {
|
||||
"WHERE client_ip = ? AND event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, update_sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
|
||||
int rc = sqlite3_step(stmt);
|
||||
int changes = sqlite3_changes(g_db);
|
||||
int changes = sqlite3_changes(db);
|
||||
sqlite3_finalize(stmt);
|
||||
if (rc != SQLITE_DONE) return -1;
|
||||
|
||||
@@ -212,7 +215,7 @@ int db_log_subscription_disconnected(const char* client_ip) {
|
||||
"INSERT INTO subscriptions (subscription_id, wsi_pointer, client_ip, event_type) "
|
||||
"VALUES ('disconnect', '', ?, 'disconnected')";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, insert_sql, -1, &stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_text(stmt, 1, client_ip, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
@@ -223,7 +226,8 @@ int db_log_subscription_disconnected(const char* client_ip) {
|
||||
}
|
||||
|
||||
int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
if (!g_db || !sub_id) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !sub_id) return -1;
|
||||
|
||||
const char* sql =
|
||||
"UPDATE subscriptions "
|
||||
@@ -231,7 +235,7 @@ int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
"WHERE subscription_id = ? AND event_type = 'created'";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, events_sent);
|
||||
sqlite3_bind_text(stmt, 2, sub_id, -1, SQLITE_TRANSIENT);
|
||||
@@ -242,7 +246,8 @@ int db_update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
}
|
||||
|
||||
int db_cleanup_orphaned_subscriptions(void) {
|
||||
if (!g_db) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return -1;
|
||||
|
||||
const char* sql =
|
||||
"UPDATE subscriptions "
|
||||
@@ -250,22 +255,23 @@ int db_cleanup_orphaned_subscriptions(void) {
|
||||
"WHERE event_type = 'created' AND ended_at IS NULL";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
int changes = sqlite3_changes(g_db);
|
||||
int changes = sqlite3_changes(db);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
return (rc == SQLITE_DONE) ? changes : -1;
|
||||
}
|
||||
|
||||
int db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_out_size) {
|
||||
if (!g_db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event_id || !pubkey_out || pubkey_out_size == 0) return -1;
|
||||
|
||||
const char* sql = "SELECT pubkey FROM events WHERE id = ?";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
||||
int rc = sqlite3_step(stmt);
|
||||
@@ -283,18 +289,19 @@ int db_get_event_pubkey(const char* event_id, char* pubkey_out, size_t pubkey_ou
|
||||
}
|
||||
|
||||
int db_delete_event_by_id(const char* event_id, const char* requester_pubkey) {
|
||||
if (!g_db || !event_id || !requester_pubkey) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event_id || !requester_pubkey) return -1;
|
||||
|
||||
const char* sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, event_id, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, requester_pubkey, -1, SQLITE_TRANSIENT);
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
int changes = sqlite3_changes(g_db);
|
||||
int changes = sqlite3_changes(db);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) return -1;
|
||||
@@ -303,7 +310,8 @@ int db_delete_event_by_id(const char* event_id, const char* requester_pubkey) {
|
||||
|
||||
int db_delete_events_by_address(const char* pubkey, int kind,
|
||||
const char* d_tag, long before_timestamp) {
|
||||
if (!g_db || !pubkey) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !pubkey) return -1;
|
||||
|
||||
const char* sql_with_d =
|
||||
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
|
||||
@@ -314,7 +322,7 @@ int db_delete_events_by_address(const char* pubkey, int kind,
|
||||
const int has_d = (d_tag && d_tag[0] != '\0');
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, has_d ? sql_with_d : sql_no_d, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -326,7 +334,7 @@ int db_delete_events_by_address(const char* pubkey, int kind,
|
||||
}
|
||||
|
||||
int rc = sqlite3_step(stmt);
|
||||
int changes = sqlite3_changes(g_db);
|
||||
int changes = sqlite3_changes(db);
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE) return -1;
|
||||
@@ -440,14 +448,15 @@ int db_count_with_sql(const char* sql, const char** bind_params, int bind_param_
|
||||
char* db_execute_readonly_query_json(const char* query, const char* request_id,
|
||||
char* error_message, size_t error_size,
|
||||
int max_rows, int timeout_ms) {
|
||||
if (!g_db || !query || !request_id || !error_message) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !query || !request_id || !error_message) return NULL;
|
||||
|
||||
sqlite3_busy_timeout(g_db, timeout_ms > 0 ? timeout_ms : 5000);
|
||||
sqlite3_busy_timeout(db, timeout_ms > 0 ? timeout_ms : 5000);
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(g_db, query, -1, &stmt, NULL);
|
||||
int rc = sqlite3_prepare_v2(db, query, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
const char* err_msg = sqlite3_errmsg(g_db);
|
||||
const char* err_msg = sqlite3_errmsg(db);
|
||||
snprintf(error_message, error_size, "SQL prepare failed: %s", err_msg ? err_msg : "unknown");
|
||||
return NULL;
|
||||
}
|
||||
@@ -560,7 +569,7 @@ char* db_execute_readonly_query_json(const char* query, const char* request_id,
|
||||
sqlite3_finalize(stmt);
|
||||
|
||||
if (rc != SQLITE_DONE && rc != SQLITE_ROW) {
|
||||
const char* err_msg = sqlite3_errmsg(g_db);
|
||||
const char* err_msg = sqlite3_errmsg(db);
|
||||
snprintf(error_message, error_size, "SQL execution failed: %s", err_msg ? err_msg : "unknown");
|
||||
cJSON_Delete(rows);
|
||||
cJSON_Delete(response);
|
||||
@@ -587,11 +596,12 @@ char* db_execute_readonly_query_json(const char* query, const char* request_id,
|
||||
}
|
||||
|
||||
int db_get_total_event_count_ll(long long* out_count) {
|
||||
if (!g_db || !out_count) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !out_count) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT COUNT(*) FROM events";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
long long count = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -604,11 +614,12 @@ int db_get_total_event_count_ll(long long* out_count) {
|
||||
}
|
||||
|
||||
int db_get_event_count_since(time_t cutoff, long long* out_count) {
|
||||
if (!g_db || !out_count) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !out_count) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT COUNT(*) FROM events WHERE created_at >= ?";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)cutoff);
|
||||
|
||||
@@ -623,11 +634,12 @@ int db_get_event_count_since(time_t cutoff, long long* out_count) {
|
||||
}
|
||||
|
||||
cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT kind, COUNT(*) as count FROM events GROUP BY kind ORDER BY count DESC";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
cJSON* rows = cJSON_CreateArray();
|
||||
if (!rows) {
|
||||
@@ -661,11 +673,12 @@ cJSON* db_get_event_kind_distribution_rows(long long* out_total_events) {
|
||||
}
|
||||
|
||||
cJSON* db_get_top_pubkeys_rows(int limit) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT pubkey, COUNT(*) as count FROM events GROUP BY pubkey ORDER BY count DESC LIMIT ?";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
sqlite3_bind_int(stmt, 1, limit > 0 ? limit : 10);
|
||||
|
||||
@@ -695,7 +708,8 @@ cJSON* db_get_top_pubkeys_rows(int limit) {
|
||||
}
|
||||
|
||||
cJSON* db_get_subscription_details_rows(void) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql =
|
||||
@@ -703,7 +717,7 @@ cJSON* db_get_subscription_details_rows(void) {
|
||||
"FROM active_subscriptions_log "
|
||||
"ORDER BY created_at DESC";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
cJSON* rows = cJSON_CreateArray();
|
||||
if (!rows) {
|
||||
@@ -743,11 +757,12 @@ cJSON* db_get_subscription_details_rows(void) {
|
||||
}
|
||||
|
||||
cJSON* db_get_all_config_rows(void) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT key, value FROM config ORDER BY key";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
cJSON* rows = cJSON_CreateArray();
|
||||
if (!rows) {
|
||||
@@ -775,11 +790,12 @@ cJSON* db_get_all_config_rows(void) {
|
||||
}
|
||||
|
||||
char* db_get_config_value_dup(const char* key) {
|
||||
if (!g_db || !key) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !key) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT value FROM config WHERE key = ?";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_TRANSIENT);
|
||||
|
||||
@@ -795,14 +811,15 @@ char* db_get_config_value_dup(const char* key) {
|
||||
|
||||
int db_set_config_value_full(const char* key, const char* value, const char* data_type,
|
||||
const char* description, const char* category, int requires_restart) {
|
||||
if (!g_db || !key || !value || !data_type) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !key || !value || !data_type) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql =
|
||||
"INSERT OR REPLACE INTO config (key, value, data_type, description, category, requires_restart) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?)";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -819,11 +836,12 @@ int db_set_config_value_full(const char* key, const char* value, const char* dat
|
||||
}
|
||||
|
||||
int db_update_config_value_only(const char* key, const char* value) {
|
||||
if (!g_db || !key || !value) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !key || !value) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "UPDATE config SET value = ?, updated_at = strftime('%s', 'now') WHERE key = ?";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -836,12 +854,13 @@ int db_update_config_value_only(const char* key, const char* value) {
|
||||
}
|
||||
|
||||
int db_upsert_config_value(const char* key, const char* value, const char* data_type) {
|
||||
if (!g_db || !key || !value || !data_type) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !key || !value || !data_type) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
|
||||
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -855,11 +874,12 @@ int db_upsert_config_value(const char* key, const char* value, const char* data_
|
||||
}
|
||||
|
||||
int db_store_relay_private_key_hex(const char* relay_privkey_hex) {
|
||||
if (!g_db || !relay_privkey_hex) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !relay_privkey_hex) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT OR REPLACE INTO relay_seckey (private_key_hex) VALUES (?)";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, relay_privkey_hex, -1, SQLITE_TRANSIENT);
|
||||
int rc = sqlite3_step(stmt);
|
||||
@@ -869,11 +889,12 @@ int db_store_relay_private_key_hex(const char* relay_privkey_hex) {
|
||||
}
|
||||
|
||||
char* db_get_relay_private_key_hex_dup(void) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT private_key_hex FROM relay_seckey";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return NULL;
|
||||
|
||||
char* result = NULL;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -886,7 +907,8 @@ char* db_get_relay_private_key_hex_dup(void) {
|
||||
}
|
||||
|
||||
int db_store_config_event(const cJSON* event) {
|
||||
if (!g_db || !event) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event) return -1;
|
||||
|
||||
cJSON* id_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "id");
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItemCaseSensitive((cJSON*)event, "pubkey");
|
||||
@@ -904,7 +926,7 @@ int db_store_config_event(const cJSON* event) {
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT OR REPLACE INTO events (id, pubkey, created_at, kind, event_type, content, sig, tags) VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
free(tags_str);
|
||||
return -1;
|
||||
}
|
||||
@@ -1017,11 +1039,12 @@ int db_insert_event_with_json(const char* id, const char* pubkey, long long crea
|
||||
}
|
||||
|
||||
int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_created_at) {
|
||||
if (!g_db || !out_min_created_at || !out_max_created_at) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !out_min_created_at || !out_max_created_at) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT MIN(created_at), MAX(created_at) FROM events";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1039,11 +1062,12 @@ int db_get_event_time_bounds(long long* out_min_created_at, long long* out_max_c
|
||||
}
|
||||
|
||||
int db_event_id_exists(const char* event_id, int* out_exists) {
|
||||
if (!g_db || !event_id || !out_exists) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event_id || !out_exists) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT 1 FROM events WHERE id=? LIMIT 1";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, event_id, 64, SQLITE_STATIC);
|
||||
int exists = (sqlite3_step(stmt) == SQLITE_ROW) ? 1 : 0;
|
||||
@@ -1054,13 +1078,14 @@ int db_event_id_exists(const char* event_id, int* out_exists) {
|
||||
}
|
||||
|
||||
cJSON* db_retrieve_event_by_id(const char* event_id) {
|
||||
if (!g_db || !event_id) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event_id) return NULL;
|
||||
|
||||
const char* sql =
|
||||
"SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE id = ?";
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1096,11 +1121,12 @@ cJSON* db_retrieve_event_by_id(const char* event_id) {
|
||||
}
|
||||
|
||||
char* db_get_latest_event_pubkey_for_kind_dup(int kind) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
const char* sql = "SELECT pubkey FROM events WHERE kind = ? ORDER BY created_at DESC LIMIT 1";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1119,11 +1145,12 @@ char* db_get_latest_event_pubkey_for_kind_dup(int kind) {
|
||||
}
|
||||
|
||||
int db_get_config_row_count(int* out_count) {
|
||||
if (!g_db || !out_count) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !out_count) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "SELECT COUNT(*) FROM config";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1138,15 +1165,16 @@ int db_get_config_row_count(int* out_count) {
|
||||
}
|
||||
|
||||
int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
|
||||
if (!g_db || !event_id || !tags || !cJSON_IsArray(tags)) {
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !event_id || !tags || !cJSON_IsArray(tags)) {
|
||||
return 0; // Not an error if no tags
|
||||
}
|
||||
|
||||
const char* sql = "INSERT INTO event_tags (event_id, tag_name, tag_value, tag_index) VALUES (?, ?, ?, ?)";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(g_db));
|
||||
DEBUG_ERROR("Failed to prepare event_tags insert: %s", sqlite3_errmsg(db));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1166,7 +1194,7 @@ int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
|
||||
|
||||
rc = sqlite3_step(stmt);
|
||||
if (rc != SQLITE_DONE) {
|
||||
DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(g_db));
|
||||
DEBUG_ERROR("Failed to insert event tag: %s", sqlite3_errmsg(db));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1178,10 +1206,11 @@ int db_store_event_tags_cjson(const char* event_id, const cJSON* tags) {
|
||||
}
|
||||
|
||||
int db_populate_event_tags_from_existing(void) {
|
||||
if (!g_db) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return -1;
|
||||
|
||||
sqlite3_stmt* check_stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, "SELECT COUNT(*) FROM event_tags", -1, &check_stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1196,10 +1225,10 @@ int db_populate_event_tags_from_existing(void) {
|
||||
|
||||
const char* sql = "SELECT id, tags FROM events WHERE tags != '[]'";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
int rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_exec(g_db, "BEGIN TRANSACTION", NULL, NULL, NULL);
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, NULL);
|
||||
|
||||
int event_count = 0;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
@@ -1217,18 +1246,19 @@ int db_populate_event_tags_from_existing(void) {
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(g_db, "COMMIT", NULL, NULL, NULL);
|
||||
sqlite3_exec(db, "COMMIT", NULL, NULL, NULL);
|
||||
|
||||
DEBUG_INFO("Populated event_tags for %d events", event_count);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
|
||||
if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "INSERT INTO auth_rules (rule_type, pattern_type, pattern_value) VALUES (?, ?, ?)";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
|
||||
@@ -1240,11 +1270,12 @@ int db_add_auth_rule(const char* rule_type, const char* pattern_type, const char
|
||||
}
|
||||
|
||||
int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const char* pattern_value) {
|
||||
if (!g_db || !rule_type || !pattern_type || !pattern_value) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !rule_type || !pattern_type || !pattern_value) return -1;
|
||||
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
const char* sql = "DELETE FROM auth_rules WHERE rule_type = ? AND pattern_type = ? AND pattern_value = ?";
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1;
|
||||
|
||||
sqlite3_bind_text(stmt, 1, rule_type, -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_text(stmt, 2, pattern_type, -1, SQLITE_TRANSIENT);
|
||||
@@ -1256,15 +1287,16 @@ int db_remove_auth_rule(const char* rule_type, const char* pattern_type, const c
|
||||
}
|
||||
|
||||
int db_delete_wot_whitelist_rules(void) {
|
||||
if (!g_db) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return -1;
|
||||
|
||||
const char* sql = "DELETE FROM auth_rules WHERE rule_type = 'wot_whitelist'";
|
||||
int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL);
|
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
||||
return (rc == SQLITE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
int db_count_wot_whitelist_rules(void) {
|
||||
if (!g_db) return 0;
|
||||
if (!db_is_available()) return 0;
|
||||
|
||||
const char* sql = "SELECT COUNT(*) FROM auth_rules WHERE rule_type = 'wot_whitelist' AND active = 1";
|
||||
int count = 0;
|
||||
@@ -1275,11 +1307,12 @@ int db_count_wot_whitelist_rules(void) {
|
||||
}
|
||||
|
||||
int db_table_exists(const char* table_name, int* out_exists) {
|
||||
if (!g_db || !table_name || !out_exists) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !table_name || !out_exists) return -1;
|
||||
|
||||
const char* sql = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=? LIMIT 1";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1290,11 +1323,12 @@ int db_table_exists(const char* table_name, int* out_exists) {
|
||||
}
|
||||
|
||||
char* db_get_schema_version_dup(void) {
|
||||
if (!g_db) return NULL;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db) return NULL;
|
||||
|
||||
const char* sql = "SELECT value FROM schema_info WHERE key = 'version'";
|
||||
sqlite3_stmt* stmt = NULL;
|
||||
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -1311,8 +1345,9 @@ char* db_get_schema_version_dup(void) {
|
||||
}
|
||||
|
||||
int db_exec_sql(const char* sql) {
|
||||
if (!g_db || !sql) return -1;
|
||||
sqlite3* db = db_active_connection();
|
||||
if (!db || !sql) return -1;
|
||||
|
||||
int rc = sqlite3_exec(g_db, sql, NULL, NULL, NULL);
|
||||
int rc = sqlite3_exec(db, sql, NULL, NULL, NULL);
|
||||
return (rc == SQLITE_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+23
-1
@@ -3,6 +3,7 @@
|
||||
#include "debug.h"
|
||||
#include "config.h"
|
||||
#include "db_ops.h"
|
||||
#include "thread_pool.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
@@ -251,6 +252,15 @@ void ip_ban_save_to_db(void) {
|
||||
|
||||
// Check if an IP is in the idle_ban_whitelist config (comma-separated list)
|
||||
static int ip_is_whitelisted(const char* ip) {
|
||||
if (!ip) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Always trust loopback to avoid local test/dev self-bans.
|
||||
if (strcmp(ip, "127.0.0.1") == 0 || strcmp(ip, "::1") == 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* whitelist = get_config_value("idle_ban_whitelist");
|
||||
if (!whitelist || whitelist[0] == '\0') {
|
||||
if (whitelist) free((char*)whitelist);
|
||||
@@ -556,7 +566,19 @@ void ip_ban_log_stats(void) {
|
||||
last_log = now;
|
||||
|
||||
// Save to DB every 5 minutes
|
||||
ip_ban_save_to_db();
|
||||
if (thread_pool_is_running()) {
|
||||
thread_pool_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.type = THREAD_POOL_JOB_IP_BAN_SAVE;
|
||||
|
||||
thread_pool_status_t rc = thread_pool_submit_write(&job, NULL);
|
||||
if (rc != THREAD_POOL_STATUS_OK) {
|
||||
DEBUG_WARN("Failed to queue IP_BAN_SAVE job (%d); saving synchronously", rc);
|
||||
ip_ban_save_to_db();
|
||||
}
|
||||
} else {
|
||||
ip_ban_save_to_db();
|
||||
}
|
||||
|
||||
pthread_mutex_lock(&g_ban_mutex);
|
||||
int auth_banned_count = 0;
|
||||
|
||||
+44
-2
@@ -981,7 +981,7 @@ int store_event_tags(const char* event_id, cJSON* tags) {
|
||||
// 1 = handled without insert (duplicate or ephemeral)
|
||||
// -1 = failure
|
||||
int store_event_core(cJSON* event) {
|
||||
if (!g_db || !event) {
|
||||
if (!db_is_available() || !event) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1143,7 +1143,7 @@ int store_event(cJSON* event) {
|
||||
// Uses the primary key index — single B-tree lookup, ~10μs.
|
||||
// Call this BEFORE signature verification to skip expensive crypto on duplicates.
|
||||
int event_id_exists_in_db(const char* event_id) {
|
||||
if (!g_db || !event_id || strlen(event_id) != 64) return 0;
|
||||
if (!db_is_available() || !event_id || strlen(event_id) != 64) return 0;
|
||||
|
||||
int exists = 0;
|
||||
if (db_event_id_exists(event_id, &exists) != 0) {
|
||||
@@ -2231,6 +2231,44 @@ int main(int argc, char* argv[]) {
|
||||
nostr_cleanup();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Pre-warm unified config cache while DB is still available.
|
||||
// This is critical because runtime strict mode sets g_db = NULL,
|
||||
// and admin authorization depends on relay_pubkey/admin_pubkey reads.
|
||||
if (reload_config_from_table() != 0) {
|
||||
DEBUG_ERROR("Failed to pre-warm configuration cache from config table");
|
||||
cleanup_configuration_system();
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char* prewarmed_relay_pubkey = get_config_value("relay_pubkey");
|
||||
const char* prewarmed_admin_pubkey = get_config_value("admin_pubkey");
|
||||
if (!prewarmed_relay_pubkey || strlen(prewarmed_relay_pubkey) != 64 ||
|
||||
!prewarmed_admin_pubkey || strlen(prewarmed_admin_pubkey) != 64) {
|
||||
if (prewarmed_relay_pubkey) free((char*)prewarmed_relay_pubkey);
|
||||
if (prewarmed_admin_pubkey) free((char*)prewarmed_admin_pubkey);
|
||||
DEBUG_ERROR("Critical config pre-warm validation failed (relay_pubkey/admin_pubkey missing)");
|
||||
cleanup_configuration_system();
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
free((char*)prewarmed_relay_pubkey);
|
||||
free((char*)prewarmed_admin_pubkey);
|
||||
|
||||
// Preload relay private key runtime cache while DB is still available.
|
||||
// Required for admin command decrypt/encrypt after strict mode sets g_db = NULL.
|
||||
if (preload_relay_private_key_cache() != 0) {
|
||||
DEBUG_ERROR("Failed to pre-warm relay private key cache");
|
||||
cleanup_configuration_system();
|
||||
nostr_cleanup();
|
||||
close_database();
|
||||
return 1;
|
||||
}
|
||||
|
||||
DEBUG_INFO("Configuration cache pre-warmed with relay_pubkey/admin_pubkey and relay private key");
|
||||
|
||||
// Configuration system is now fully initialized with event-based approach
|
||||
// All configuration is loaded from database events
|
||||
@@ -2315,6 +2353,10 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 5 strict mode: remove main-thread fallback to global DB handle.
|
||||
// Runtime DB access must go through thread-bound worker connections.
|
||||
g_db = NULL;
|
||||
|
||||
// Start WebSocket Nostr relay server (port from CLI override or configuration)
|
||||
int result = start_websocket_relay(cli_options.port_override, cli_options.strict_port); // Use CLI port override if specified, otherwise config
|
||||
|
||||
|
||||
+2
-2
@@ -13,8 +13,8 @@
|
||||
// Using CRELAY_ prefix to avoid conflicts with nostr_core_lib VERSION macros
|
||||
#define CRELAY_VERSION_MAJOR 2
|
||||
#define CRELAY_VERSION_MINOR 1
|
||||
#define CRELAY_VERSION_PATCH 5
|
||||
#define CRELAY_VERSION "v2.1.5"
|
||||
#define CRELAY_VERSION_PATCH 6
|
||||
#define CRELAY_VERSION "v2.1.6"
|
||||
|
||||
// Relay metadata (authoritative source for NIP-11 information)
|
||||
#define RELAY_NAME "C-Relay"
|
||||
|
||||
+3
-3
@@ -8,7 +8,7 @@
|
||||
#include <libwebsockets.h>
|
||||
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||
#include "config.h"
|
||||
|
||||
#include "main.h"
|
||||
|
||||
// Forward declarations for configuration functions
|
||||
const char* get_config_value(const char* key);
|
||||
@@ -246,14 +246,14 @@ cJSON* generate_relay_info_json() {
|
||||
cJSON_AddStringToObject(info, "software", relay_software);
|
||||
free((char*)relay_software);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "software", "https://git.laantungir.net/laantungir/c-relay.git");
|
||||
cJSON_AddStringToObject(info, "software", RELAY_SOFTWARE);
|
||||
}
|
||||
|
||||
if (relay_version && strlen(relay_version) > 0) {
|
||||
cJSON_AddStringToObject(info, "version", relay_version);
|
||||
free((char*)relay_version);
|
||||
} else {
|
||||
cJSON_AddStringToObject(info, "version", "0.2.0");
|
||||
cJSON_AddStringToObject(info, "version", RELAY_VERSION);
|
||||
}
|
||||
|
||||
// Add policies
|
||||
|
||||
+146
-4
@@ -2,6 +2,7 @@
|
||||
#include <cjson/cJSON.h>
|
||||
#include "debug.h"
|
||||
#include "db_ops.h"
|
||||
#include "thread_pool.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
@@ -10,6 +11,34 @@
|
||||
#include <libwebsockets.h>
|
||||
#include "subscriptions.h"
|
||||
|
||||
static void free_sub_log_created_payload_local(thread_pool_sub_log_created_payload_t* payload) {
|
||||
if (!payload) return;
|
||||
free(payload->sub_id);
|
||||
free(payload->wsi_ptr);
|
||||
free(payload->client_ip);
|
||||
free(payload->filter_json);
|
||||
free(payload);
|
||||
}
|
||||
|
||||
static void free_sub_log_closed_payload_local(thread_pool_sub_log_closed_payload_t* payload) {
|
||||
if (!payload) return;
|
||||
free(payload->sub_id);
|
||||
free(payload->client_ip);
|
||||
free(payload);
|
||||
}
|
||||
|
||||
static void free_sub_log_disconnected_payload_local(thread_pool_sub_log_disconnected_payload_t* payload) {
|
||||
if (!payload) return;
|
||||
free(payload->client_ip);
|
||||
free(payload);
|
||||
}
|
||||
|
||||
static void free_sub_update_events_payload_local(thread_pool_sub_update_events_payload_t* payload) {
|
||||
if (!payload) return;
|
||||
free(payload->sub_id);
|
||||
free(payload);
|
||||
}
|
||||
|
||||
// Forward declarations for logging functions
|
||||
|
||||
// Forward declarations for configuration functions
|
||||
@@ -1040,7 +1069,43 @@ void log_subscription_created(const subscription_t* sub) {
|
||||
cJSON_Delete(filters_array);
|
||||
}
|
||||
|
||||
db_log_subscription_created(sub->id, wsi_str, sub->client_ip, filter_json ? filter_json : "[]");
|
||||
if (!thread_pool_is_running()) {
|
||||
db_log_subscription_created(sub->id, wsi_str, sub->client_ip, filter_json ? filter_json : "[]");
|
||||
if (filter_json) free(filter_json);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_sub_log_created_payload_t* payload = calloc(1, sizeof(*payload));
|
||||
if (!payload) {
|
||||
if (filter_json) free(filter_json);
|
||||
return;
|
||||
}
|
||||
|
||||
payload->sub_id = strdup(sub->id);
|
||||
payload->wsi_ptr = strdup(wsi_str);
|
||||
payload->client_ip = strdup(sub->client_ip);
|
||||
payload->filter_json = strdup(filter_json ? filter_json : "[]");
|
||||
|
||||
if (!payload->sub_id || !payload->wsi_ptr || !payload->client_ip || !payload->filter_json) {
|
||||
free(payload->sub_id);
|
||||
free(payload->wsi_ptr);
|
||||
free(payload->client_ip);
|
||||
free(payload->filter_json);
|
||||
free(payload);
|
||||
if (filter_json) free(filter_json);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.type = THREAD_POOL_JOB_LOG_SUB_CREATED;
|
||||
job.payload = payload;
|
||||
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_created_payload_local;
|
||||
|
||||
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
|
||||
if (submit_rc != THREAD_POOL_STATUS_OK) {
|
||||
free_sub_log_created_payload_local(payload);
|
||||
}
|
||||
|
||||
if (filter_json) free(filter_json);
|
||||
}
|
||||
@@ -1050,14 +1115,65 @@ void log_subscription_closed(const char* sub_id, const char* client_ip, const ch
|
||||
(void)reason; // Mark as intentionally unused
|
||||
if (!sub_id) return;
|
||||
|
||||
db_log_subscription_closed(sub_id, client_ip);
|
||||
if (!thread_pool_is_running()) {
|
||||
db_log_subscription_closed(sub_id, client_ip);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_sub_log_closed_payload_t* payload = calloc(1, sizeof(*payload));
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload->sub_id = strdup(sub_id);
|
||||
payload->client_ip = strdup(client_ip ? client_ip : "unknown");
|
||||
if (!payload->sub_id || !payload->client_ip) {
|
||||
free_sub_log_closed_payload_local(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.type = THREAD_POOL_JOB_LOG_SUB_CLOSED;
|
||||
job.payload = payload;
|
||||
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_closed_payload_local;
|
||||
|
||||
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
|
||||
if (submit_rc != THREAD_POOL_STATUS_OK) {
|
||||
free_sub_log_closed_payload_local(payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Log subscription disconnection to database
|
||||
void log_subscription_disconnected(const char* client_ip) {
|
||||
if (!client_ip) return;
|
||||
|
||||
db_log_subscription_disconnected(client_ip);
|
||||
if (!thread_pool_is_running()) {
|
||||
db_log_subscription_disconnected(client_ip);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_sub_log_disconnected_payload_t* payload = calloc(1, sizeof(*payload));
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload->client_ip = strdup(client_ip);
|
||||
if (!payload->client_ip) {
|
||||
free_sub_log_disconnected_payload_local(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.type = THREAD_POOL_JOB_LOG_SUB_DISCONNECTED;
|
||||
job.payload = payload;
|
||||
job.payload_free = (thread_pool_payload_free_cb)free_sub_log_disconnected_payload_local;
|
||||
|
||||
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
|
||||
if (submit_rc != THREAD_POOL_STATUS_OK) {
|
||||
free_sub_log_disconnected_payload_local(payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Log event broadcast to database (optional, can be resource intensive)
|
||||
@@ -1085,7 +1201,33 @@ void log_subscription_disconnected(const char* client_ip) {
|
||||
void update_subscription_events_sent(const char* sub_id, int events_sent) {
|
||||
if (!sub_id) return;
|
||||
|
||||
db_update_subscription_events_sent(sub_id, events_sent);
|
||||
if (!thread_pool_is_running()) {
|
||||
db_update_subscription_events_sent(sub_id, events_sent);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_sub_update_events_payload_t* payload = calloc(1, sizeof(*payload));
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
payload->sub_id = strdup(sub_id);
|
||||
payload->events_sent = events_sent;
|
||||
if (!payload->sub_id) {
|
||||
free_sub_update_events_payload_local(payload);
|
||||
return;
|
||||
}
|
||||
|
||||
thread_pool_job_t job;
|
||||
memset(&job, 0, sizeof(job));
|
||||
job.type = THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT;
|
||||
job.payload = payload;
|
||||
job.payload_free = (thread_pool_payload_free_cb)free_sub_update_events_payload_local;
|
||||
|
||||
thread_pool_status_t submit_rc = thread_pool_submit_write(&job, NULL);
|
||||
if (submit_rc != THREAD_POOL_STATUS_OK) {
|
||||
free_sub_update_events_payload_local(payload);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup all subscriptions on startup
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "thread_pool.h"
|
||||
#include "debug.h"
|
||||
#include "db_ops.h"
|
||||
#include "ip_ban.h"
|
||||
|
||||
#include <sqlite3.h>
|
||||
#include <pthread.h>
|
||||
@@ -371,6 +372,87 @@ static void execute_store_event_job(thread_pool_job_node_t* node) {
|
||||
"STORE_EVENT completed", out, sizeof(*out));
|
||||
}
|
||||
|
||||
static void execute_sub_log_created_job(thread_pool_job_node_t* node) {
|
||||
thread_pool_sub_log_created_payload_t* payload = (thread_pool_sub_log_created_payload_t*)node->job.payload;
|
||||
if (!payload || !payload->sub_id || !payload->wsi_ptr || !payload->client_ip) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_CREATED payload missing", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_log_subscription_created(payload->sub_id,
|
||||
payload->wsi_ptr,
|
||||
payload->client_ip,
|
||||
payload->filter_json ? payload->filter_json : "[]") != 0) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_CREATED failed", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
|
||||
"SUB_LOG_CREATED completed", NULL, 0);
|
||||
}
|
||||
|
||||
static void execute_sub_log_closed_job(thread_pool_job_node_t* node) {
|
||||
thread_pool_sub_log_closed_payload_t* payload = (thread_pool_sub_log_closed_payload_t*)node->job.payload;
|
||||
if (!payload || !payload->sub_id) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_CLOSED payload missing", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_log_subscription_closed(payload->sub_id, payload->client_ip) != 0) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_CLOSED failed", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
|
||||
"SUB_LOG_CLOSED completed", NULL, 0);
|
||||
}
|
||||
|
||||
static void execute_sub_log_disconnected_job(thread_pool_job_node_t* node) {
|
||||
thread_pool_sub_log_disconnected_payload_t* payload = (thread_pool_sub_log_disconnected_payload_t*)node->job.payload;
|
||||
if (!payload || !payload->client_ip) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_DISCONNECTED payload missing", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_log_subscription_disconnected(payload->client_ip) < 0) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_LOG_DISCONNECTED failed", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
|
||||
"SUB_LOG_DISCONNECTED completed", NULL, 0);
|
||||
}
|
||||
|
||||
static void execute_sub_update_events_job(thread_pool_job_node_t* node) {
|
||||
thread_pool_sub_update_events_payload_t* payload = (thread_pool_sub_update_events_payload_t*)node->job.payload;
|
||||
if (!payload || !payload->sub_id) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_UPDATE_EVENTS payload missing", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (db_update_subscription_events_sent(payload->sub_id, payload->events_sent) != 0) {
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_INTERNAL_ERROR,
|
||||
"SUB_UPDATE_EVENTS failed", NULL, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
|
||||
"SUB_UPDATE_EVENTS completed", NULL, 0);
|
||||
}
|
||||
|
||||
static void execute_ip_ban_save_job(thread_pool_job_node_t* node) {
|
||||
ip_ban_save_to_db();
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_OK,
|
||||
"IP_BAN_SAVE completed", NULL, 0);
|
||||
}
|
||||
|
||||
static void execute_read_job(thread_pool_job_node_t* node) {
|
||||
switch (node->job.type) {
|
||||
case THREAD_POOL_JOB_REQ_QUERY:
|
||||
@@ -391,6 +473,21 @@ static void execute_write_job(thread_pool_job_node_t* node) {
|
||||
case THREAD_POOL_JOB_STORE_EVENT:
|
||||
execute_store_event_job(node);
|
||||
break;
|
||||
case THREAD_POOL_JOB_LOG_SUB_CREATED:
|
||||
execute_sub_log_created_job(node);
|
||||
break;
|
||||
case THREAD_POOL_JOB_LOG_SUB_CLOSED:
|
||||
execute_sub_log_closed_job(node);
|
||||
break;
|
||||
case THREAD_POOL_JOB_LOG_SUB_DISCONNECTED:
|
||||
execute_sub_log_disconnected_job(node);
|
||||
break;
|
||||
case THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT:
|
||||
execute_sub_update_events_job(node);
|
||||
break;
|
||||
case THREAD_POOL_JOB_IP_BAN_SAVE:
|
||||
execute_ip_ban_save_job(node);
|
||||
break;
|
||||
default:
|
||||
complete_job_with_result(node, THREAD_POOL_STATUS_NOT_IMPLEMENTED,
|
||||
"Write job type not implemented", NULL, 0);
|
||||
|
||||
@@ -13,6 +13,11 @@ typedef enum {
|
||||
THREAD_POOL_JOB_COUNT_QUERY,
|
||||
THREAD_POOL_JOB_STORE_EVENT,
|
||||
THREAD_POOL_JOB_DELETE_EVENT,
|
||||
THREAD_POOL_JOB_LOG_SUB_CREATED,
|
||||
THREAD_POOL_JOB_LOG_SUB_CLOSED,
|
||||
THREAD_POOL_JOB_LOG_SUB_DISCONNECTED,
|
||||
THREAD_POOL_JOB_UPDATE_SUB_EVENTS_SENT,
|
||||
THREAD_POOL_JOB_IP_BAN_SAVE,
|
||||
THREAD_POOL_JOB_CUSTOM
|
||||
} thread_pool_job_type_t;
|
||||
|
||||
@@ -95,6 +100,27 @@ typedef struct {
|
||||
int extended_errcode;
|
||||
} thread_pool_store_event_result_t;
|
||||
|
||||
typedef struct {
|
||||
char* sub_id;
|
||||
char* wsi_ptr;
|
||||
char* client_ip;
|
||||
char* filter_json;
|
||||
} thread_pool_sub_log_created_payload_t;
|
||||
|
||||
typedef struct {
|
||||
char* sub_id;
|
||||
char* client_ip;
|
||||
} thread_pool_sub_log_closed_payload_t;
|
||||
|
||||
typedef struct {
|
||||
char* client_ip;
|
||||
} thread_pool_sub_log_disconnected_payload_t;
|
||||
|
||||
typedef struct {
|
||||
char* sub_id;
|
||||
int events_sent;
|
||||
} thread_pool_sub_update_events_payload_t;
|
||||
|
||||
int thread_pool_init(const thread_pool_config_t* config);
|
||||
void thread_pool_shutdown(void);
|
||||
int thread_pool_is_running(void);
|
||||
|
||||
+53
-6
@@ -18,6 +18,8 @@
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <sqlite3.h>
|
||||
|
||||
// Include nostr_core_lib for Nostr functionality
|
||||
#include "../nostr_core_lib/cjson/cJSON.h"
|
||||
#include "../nostr_core_lib/nostr_core/nostr_core.h"
|
||||
@@ -137,6 +139,26 @@ static pthread_mutex_t g_connections_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
int get_active_connection_count(void) { return g_connection_count; }
|
||||
|
||||
int websocket_get_live_pss(struct lws* wsi, struct per_session_data** out_pss) {
|
||||
if (!wsi || !out_pss) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
*out_pss = NULL;
|
||||
|
||||
pthread_mutex_lock(&g_connections_lock);
|
||||
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
|
||||
if (g_connections[i].wsi == wsi && g_connections[i].pss != NULL) {
|
||||
*out_pss = g_connections[i].pss;
|
||||
pthread_mutex_unlock(&g_connections_lock);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
pthread_mutex_unlock(&g_connections_lock);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void connection_list_add(struct lws* wsi, struct per_session_data* pss) {
|
||||
pthread_mutex_lock(&g_connections_lock);
|
||||
for (int i = 0; i < MAX_TRACKED_CONNECTIONS; i++) {
|
||||
@@ -477,8 +499,8 @@ static int event_is_async_eligible(cJSON* event, int* out_kind, char event_id_ou
|
||||
|
||||
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
|
||||
// Keep special/admin paths on main thread for existing behavior.
|
||||
if (kind == 14 || kind == 1059 || kind == 23456) {
|
||||
// Keep admin command path on main thread for existing behavior.
|
||||
if (kind == 23456) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -509,6 +531,24 @@ static void* async_event_worker_main(void* arg) {
|
||||
(void)arg;
|
||||
pthread_setname_np(pthread_self(), "event-worker");
|
||||
|
||||
sqlite3* worker_db = NULL;
|
||||
const char* db_path = db_get_database_path();
|
||||
if (db_path && db_path[0] != '\0') {
|
||||
if (sqlite3_open_v2(db_path, &worker_db, SQLITE_OPEN_READWRITE, NULL) == SQLITE_OK) {
|
||||
sqlite3_exec(worker_db, "PRAGMA journal_mode=WAL;", NULL, NULL, NULL);
|
||||
sqlite3_busy_timeout(worker_db, 5000);
|
||||
db_set_thread_connection(worker_db);
|
||||
} else {
|
||||
if (worker_db) {
|
||||
sqlite3_close(worker_db);
|
||||
worker_db = NULL;
|
||||
}
|
||||
DEBUG_WARN("event-worker: failed to open dedicated DB connection; using default connection context");
|
||||
}
|
||||
} else {
|
||||
DEBUG_WARN("event-worker: database path unavailable; using default connection context");
|
||||
}
|
||||
|
||||
while (g_async_event_worker_running) {
|
||||
async_event_job_t* job = async_event_job_pop_blocking();
|
||||
if (!job) {
|
||||
@@ -579,6 +619,11 @@ static void* async_event_worker_main(void* arg) {
|
||||
free(job);
|
||||
}
|
||||
|
||||
if (worker_db) {
|
||||
db_clear_thread_connection();
|
||||
sqlite3_close(worker_db);
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -680,8 +725,9 @@ static int try_submit_async_event(cJSON* event, const char* event_json, struct l
|
||||
static void process_async_event_completions(void) {
|
||||
async_event_completion_t* completion = NULL;
|
||||
while ((completion = async_event_completion_pop()) != NULL) {
|
||||
struct per_session_data* current_pss = (struct per_session_data*)lws_wsi_user(completion->wsi);
|
||||
int target_alive = (current_pss && current_pss == completion->pss_token);
|
||||
struct per_session_data* current_pss = NULL;
|
||||
int target_alive = websocket_get_live_pss(completion->wsi, ¤t_pss) &&
|
||||
current_pss == completion->pss_token;
|
||||
|
||||
int needs_event_obj = completion->run_post_actions ||
|
||||
(target_alive && completion->success && completion->should_broadcast);
|
||||
@@ -896,8 +942,9 @@ static void process_count_async_completions(void) {
|
||||
|
||||
state->pending_jobs--;
|
||||
if (state->pending_jobs <= 0) {
|
||||
struct per_session_data* current_pss = (struct per_session_data*)lws_wsi_user(state->wsi);
|
||||
int target_alive = (current_pss && current_pss == state->pss_token);
|
||||
struct per_session_data* current_pss = NULL;
|
||||
int target_alive = websocket_get_live_pss(state->wsi, ¤t_pss) &&
|
||||
current_pss == state->pss_token;
|
||||
if (target_alive) {
|
||||
if (state->had_error) {
|
||||
send_notice_message(state->wsi, current_pss, state->error_message[0] ? state->error_message : "error: count query failed");
|
||||
|
||||
@@ -99,6 +99,10 @@ struct per_session_data {
|
||||
// Get current active WebSocket connection count
|
||||
int get_active_connection_count(void);
|
||||
|
||||
// Safely resolve per-session data only for currently tracked live WebSocket sessions.
|
||||
// Returns 1 and sets *out_pss on success, otherwise returns 0.
|
||||
int websocket_get_live_pss(struct lws* wsi, struct per_session_data** out_pss);
|
||||
|
||||
// Actual relay port bound by libwebsockets at runtime (after fallback/retry logic)
|
||||
extern int g_relay_port;
|
||||
|
||||
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
Profiling PID: 2130230
|
||||
CLK_TCK=100
|
||||
Starting perf record in background...
|
||||
perf_pid=2131008
|
||||
Done. Artifacts in /tmp/c_relay_thread_profile_20260401_202647
|
||||
@@ -0,0 +1,121 @@
|
||||
timestamp,tid,thread_name,cpu_pct,delta_ticks,total_ticks
|
||||
1775075209,2130230,lws-main,0.00,0,17777
|
||||
1775075209,2130232,db-read-1,0.00,0,84
|
||||
1775075209,2130233,db-write,0.00,0,15
|
||||
1775075209,2130234,event-worker,0.00,0,1
|
||||
1775075219,2130230,lws-main,21.50,215,17992
|
||||
1775075219,2130232,db-read-1,0.80,8,92
|
||||
1775075219,2130233,db-write,0.00,0,15
|
||||
1775075219,2130234,event-worker,0.00,0,1
|
||||
1775075229,2130230,lws-main,50.40,504,18496
|
||||
1775075229,2130232,db-read-1,0.70,7,99
|
||||
1775075229,2130233,db-write,0.00,0,15
|
||||
1775075229,2130234,event-worker,0.00,0,1
|
||||
1775075239,2130230,lws-main,90.10,901,19397
|
||||
1775075239,2130232,db-read-1,0.30,3,102
|
||||
1775075239,2130233,db-write,0.00,0,15
|
||||
1775075239,2130234,event-worker,0.00,0,1
|
||||
1775075249,2130230,lws-main,100.10,1001,20398
|
||||
1775075249,2130232,db-read-1,0.00,0,102
|
||||
1775075249,2130233,db-write,0.00,0,15
|
||||
1775075249,2130234,event-worker,0.00,0,1
|
||||
1775075259,2130230,lws-main,99.40,994,21392
|
||||
1775075259,2130232,db-read-1,0.50,5,107
|
||||
1775075259,2130233,db-write,0.00,0,15
|
||||
1775075259,2130234,event-worker,0.00,0,1
|
||||
1775075269,2130230,lws-main,78.00,780,22172
|
||||
1775075269,2130232,db-read-1,0.60,6,113
|
||||
1775075269,2130233,db-write,0.00,0,15
|
||||
1775075269,2130234,event-worker,0.00,0,1
|
||||
1775075279,2130230,lws-main,14.40,144,22316
|
||||
1775075279,2130232,db-read-1,0.20,2,115
|
||||
1775075279,2130233,db-write,0.00,0,15
|
||||
1775075279,2130234,event-worker,0.00,0,1
|
||||
1775075289,2130230,lws-main,49.60,496,22812
|
||||
1775075289,2130232,db-read-1,0.60,6,121
|
||||
1775075289,2130233,db-write,0.00,0,15
|
||||
1775075289,2130234,event-worker,0.00,0,1
|
||||
1775075299,2130230,lws-main,91.70,917,23729
|
||||
1775075299,2130232,db-read-1,0.20,2,123
|
||||
1775075299,2130233,db-write,0.00,0,15
|
||||
1775075299,2130234,event-worker,0.00,0,1
|
||||
1775075309,2130230,lws-main,77.90,779,24508
|
||||
1775075309,2130232,db-read-1,0.60,6,129
|
||||
1775075309,2130233,db-write,0.00,0,15
|
||||
1775075309,2130234,event-worker,0.00,0,1
|
||||
1775075319,2130230,lws-main,87.40,874,25382
|
||||
1775075319,2130232,db-read-1,0.20,2,131
|
||||
1775075319,2130233,db-write,0.00,0,15
|
||||
1775075319,2130234,event-worker,0.00,0,1
|
||||
1775075329,2130230,lws-main,67.40,674,26056
|
||||
1775075329,2130232,db-read-1,0.10,1,132
|
||||
1775075329,2130233,db-write,0.00,0,15
|
||||
1775075329,2130234,event-worker,0.00,0,1
|
||||
1775075339,2130230,lws-main,100.20,1002,27058
|
||||
1775075339,2130232,db-read-1,0.00,0,132
|
||||
1775075339,2130233,db-write,0.00,0,15
|
||||
1775075339,2130234,event-worker,0.00,0,1
|
||||
1775075349,2130230,lws-main,100.00,1000,28058
|
||||
1775075349,2130232,db-read-1,0.10,1,133
|
||||
1775075349,2130233,db-write,0.00,0,15
|
||||
1775075349,2130234,event-worker,0.00,0,1
|
||||
1775075359,2130230,lws-main,100.10,1001,29059
|
||||
1775075359,2130232,db-read-1,0.10,1,134
|
||||
1775075359,2130233,db-write,0.10,1,16
|
||||
1775075359,2130234,event-worker,0.00,0,1
|
||||
1775075369,2130230,lws-main,100.10,1001,30060
|
||||
1775075369,2130232,db-read-1,0.20,2,136
|
||||
1775075369,2130233,db-write,0.00,0,16
|
||||
1775075369,2130234,event-worker,0.00,0,1
|
||||
1775075379,2130230,lws-main,18.30,183,30243
|
||||
1775075379,2130232,db-read-1,0.40,4,140
|
||||
1775075379,2130233,db-write,0.00,0,16
|
||||
1775075379,2130234,event-worker,0.10,1,2
|
||||
1775075389,2130230,lws-main,67.40,674,30917
|
||||
1775075389,2130232,db-read-1,0.10,1,141
|
||||
1775075389,2130233,db-write,0.10,1,17
|
||||
1775075389,2130234,event-worker,0.00,0,2
|
||||
1775075399,2130230,lws-main,86.00,860,31777
|
||||
1775075399,2130232,db-read-1,0.10,1,142
|
||||
1775075399,2130233,db-write,0.00,0,17
|
||||
1775075399,2130234,event-worker,0.00,0,2
|
||||
1775075409,2130230,lws-main,72.00,720,32497
|
||||
1775075409,2130232,db-read-1,0.50,5,147
|
||||
1775075409,2130233,db-write,0.00,0,17
|
||||
1775075409,2130234,event-worker,0.00,0,2
|
||||
1775075419,2130230,lws-main,21.40,214,32711
|
||||
1775075419,2130232,db-read-1,0.10,1,148
|
||||
1775075419,2130233,db-write,0.00,0,17
|
||||
1775075419,2130234,event-worker,0.00,0,2
|
||||
1775075429,2130230,lws-main,43.20,432,33143
|
||||
1775075429,2130232,db-read-1,0.00,0,148
|
||||
1775075429,2130233,db-write,0.00,0,17
|
||||
1775075429,2130234,event-worker,0.00,0,2
|
||||
1775075439,2130230,lws-main,15.40,154,33297
|
||||
1775075439,2130232,db-read-1,0.00,0,148
|
||||
1775075439,2130233,db-write,0.30,3,20
|
||||
1775075439,2130234,event-worker,0.00,0,2
|
||||
1775075449,2130230,lws-main,50.80,508,33805
|
||||
1775075449,2130232,db-read-1,0.50,5,153
|
||||
1775075449,2130233,db-write,0.00,0,20
|
||||
1775075449,2130234,event-worker,0.00,0,2
|
||||
1775075459,2130230,lws-main,37.00,370,34175
|
||||
1775075459,2130232,db-read-1,0.20,2,155
|
||||
1775075459,2130233,db-write,0.00,0,20
|
||||
1775075459,2130234,event-worker,0.00,0,2
|
||||
1775075469,2130230,lws-main,89.20,892,35067
|
||||
1775075469,2130232,db-read-1,0.20,2,157
|
||||
1775075469,2130233,db-write,0.00,0,20
|
||||
1775075469,2130234,event-worker,0.00,0,2
|
||||
1775075479,2130230,lws-main,99.20,992,36059
|
||||
1775075479,2130232,db-read-1,0.60,6,163
|
||||
1775075479,2130233,db-write,0.10,1,21
|
||||
1775075479,2130234,event-worker,0.10,1,3
|
||||
1775075489,2130230,lws-main,100.00,1000,37059
|
||||
1775075489,2130232,db-read-1,0.10,1,164
|
||||
1775075489,2130233,db-write,0.00,0,21
|
||||
1775075489,2130234,event-worker,0.00,0,3
|
||||
1775075499,2130230,lws-main,99.70,997,38056
|
||||
1775075499,2130232,db-read-1,0.20,2,166
|
||||
1775075499,2130233,db-write,0.00,0,21
|
||||
1775075499,2130234,event-worker,0.10,1,4
|
||||
|
@@ -0,0 +1,11 @@
|
||||
==========================================
|
||||
Thread CPU Summary (avg + max over run)
|
||||
==========================================
|
||||
Run timestamp (UTC): 20260401_202647
|
||||
Duration: 300s, Interval: 10s
|
||||
|
||||
2130230 lws-main 67.60 100.20
|
||||
2130232 db-read-1 0.27 0.80
|
||||
2130233 db-write 0.02 0.30
|
||||
2130234 event-worker 0.01 0.10
|
||||
TID THREAD AVG_CPU% MAX_CPU%
|
||||
Reference in New Issue
Block a user