Skip to content

feat: SQLite + PostgreSQL + MariaDB + ODBC behind a pluggable backend-ops registry#31

Open
Admnwk wants to merge 33 commits into
FiveTechSoft:mainfrom
Admnwk:integration/openadsplus
Open

feat: SQLite + PostgreSQL + MariaDB + ODBC behind a pluggable backend-ops registry#31
Admnwk wants to merge 33 commits into
FiveTechSoft:mainfrom
Admnwk:integration/openadsplus

Conversation

@Admnwk

@Admnwk Admnwk commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

This folds the optional SQL table-driver backends — SQLite, PostgreSQL, MariaDB and a generic ODBC driver — behind the ACE ABI into a single buildable line, routed through a small pluggable backend-ops registry so the per-backend dispatch no longer multiplies across the ~17 navigation / field ABI functions.

It resolves the pairwise merge conflict between the separate backend PRs (#22 / #23 / #24): each adds its own if (get_X_table(h)) { … } block to the same 17 Ads* functions, so combining N backends produces ~17×N conflicting hunks in ace_exports.cpp. The registry confines each backend to one ops struct + one registration line; the 17 ABI functions become backend-agnostic, and the native / tcp:// remote path stays the unchanged fall-through.

Mechanism

A BackendTableOps vtable (17 function pointers mirroring the table ops) is registered per HandleKind. Each ABI function does one backend_table_ops_for(h) lookup and dispatches if non-null, else runs the unchanged native/remote body. Each backend lifts its dispatch into <backend>_<op>() functions exposed via <backend>_table_ops() and registered once in register_builtin_backends() (the only place with per-backend #if).

What's here (stacked)

This branch is stacked on the not-yet-merged base PRs, so the diff includes them:

Verification (MSVC x64, all backends ON together)

  • Existing unit suite: every prior case green — behavior-preserving refactor, no test edits.
  • PostgreSQL e2e: 41/41 assertions vs a live server.
  • MariaDB e2e: 45/45 assertions vs a live server.
  • ODBC e2e: 59/59 assertions vs a throwaway Microsoft Access fixture.
  • All four backends link into one DLL; the native local DBF/ADT and native client/server paths do not register ops and run unchanged.

Adding a further backend (Firebird, or MSSQL/Oracle via ODBC) is one ops struct + one registration line.

DEVAI and others added 28 commits June 18, 2026 13:37
Wire Mg telemetry + Studio HTTP sessions. See tools/openmonitor/README.md.
A connection opened with a sqlite://<path> URI now exposes a real SQLite table through the existing ACE ABI: AdsOpenTable, GoTop/GoBottom/Skip, GetField/GetString, AtEOF/AtBOF, GetRecordCount and indexed AdsSeek all run against the database, so a Harbour rddads application reaches it with no code change.

Implementation mirrors the existing tcp:// dual-mode dispatch (RemoteConnection/RemoteTable): a thin SqliteConnection/SqliteTable pair dispatched at the ABI edge, leaving the DBF/CDX engine untouched. Record numbers map to stable SQLite rowids; the driver opens the database read-only (SQLITE_OPEN_READONLY) in this change.

Gated behind OPENADS_WITH_SQLITE (ON by default). The amalgamation is built from a locally vendored copy when present, otherwise fetched via FetchContent; it is never committed. Two end-to-end doctest cases seed a fixture database through the SQLite C API and drive it entirely through the ACE ABI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- sqlite_field_index: guard st->conn before dereference (it is nulled on still-open tables by AdsDisconnect to avoid use-after-free)

- skip: correct navigation from BOF/EOF when not positioned (skip(n) from BOF advances n; skip(-1) from EOF lands on the last row)

- position_at_rowid: binary-search the ascending rowid snapshot (O(log N)) instead of a linear scan

- format_sqlite_value: %.17g instead of std::to_string for doubles (no precision loss)

- is_safe_identifier: locale-independent ASCII check

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
libpq backend with postgresql:// URI, parameterized queries, E2E doctest (seed via OPENADS_TEST_PG_URI), and NMake build script. No third-party SQL amalgamation.
Add pgsql:// URI alias and compile-time disabled rejection.

Fix doctest tests: default local PG URI, correct OpenIndex array capacity.
Eliminates libgcc_s_seh-1.dll dependency; add run_postgres_tests.bat wrapper.
AdsExecuteSQLDirect on a sqlite:// connection now runs the statement directly on SQLite: DDL/DML (CREATE/INSERT/UPDATE/DELETE) execute and return no cursor; result-producing statements (SELECT/WITH/VALUES/PRAGMA/EXPLAIN) return a materialized, navigable cursor surfaced through the existing SqliteTable ABI arms. A Harbour rddads application gets full SQL + DDL — read AND create — not just xBase navigation.

The backend now opens read-write (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) so passthrough can create and mutate the database.

Covered by an end-to-end doctest that CREATEs a table, INSERTs rows and SELECTs them back through the ACE ABI, then reopens the base table to confirm the writes persisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Let SQLite classify the statement: prepare once and check sqlite3_column_count — 0 columns means execute and return no cursor, >0 means a materialized result cursor. Removes the fragile SQL keyword parsing in the ABI layer (now handles leading comments, CTEs, etc.).

- sqlite_field_index: guard pucField before to_internal (both field resolvers) to avoid a null-pointer dereference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ctid is not a stable row identifier (it moves on UPDATE/VACUUM), so it is unsafe as the cursor key for write-back. Switch open_table snapshot, load_current_row and seek_index to a primary-key snapshot mirroring the MariaDB backend, binding values with parameterized PQexecParams. PK columns are discovered from information_schema. ABI read+seek e2e green against PostgreSQL 17.10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build/test helper scripts no longer hard-code an environment-specific toolchain root fallback; they require OPENADS_TOOLCHAIN_ROOT to point at the directory holding the msvc, winlibs and pgsql/mariadb client dependencies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- load_current_row selects an explicit column list in schema order instead of SELECT *, so current_row stays aligned with the cached field order even if physical and logical column order differ. - seek_index ORDER BY now appends the primary key as a tie-breaker, making duplicate-key seeks deterministic. - quote_ident doubles any embedded double-quote (identifier safety). - field_index_ci compares case-insensitively without allocating a string per field.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AdsSetString already had a get_remote_table branch, but AdsSetDouble fell
straight through to the local-only get_table() after the prepared-statement
param check. A numeric set against a tcp:// table therefore failed silently
with [internal] unknown table, dropping the write -- so a navigational
APPEND of a row with a numeric field left that field blank on the server and
a later seek/scan could not match it.

Mirror AdsSetString: format the value locale-independently (%.17g) and send
it through the wire set_field. Proven against a live openads_serverd: an ORM
navigational CRUD cycle (create/find/update/delete + find-after-delete) over
tcp:// now passes 13/13 with zero caveats; the local DBF path is unchanged.

Sibling gap: AdsSetLogical has the same missing remote branch (no remote
logical-field test yet -- tracked, not fixed here).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tes)

Same gap as AdsSetDouble (sibling): AdsSetLogical had no get_remote_table
branch, so a logical set against a tcp:// table fell through to the local-only
get_table() and failed silently. Mirror AdsSetString/AdsSetDouble: send the
DBF logical literal ('T'/'F') through the wire set_field.

Audit of all AdsSet* setters: AdsSetString/Double/Logical now have remote
branches; AdsSetField/Empty/Money/Short/Time/LongLong reach remote via
delegation to those. Remaining non-delegating gaps (tracked, niche, no test
yet): AdsSetBinary, AdsSetJulian, AdsSetMilliseconds.

Verified: ORM nav CRUD with a logical column over tcp:// now passes 14/14
(was failing 'model logical ativo true'); sqlite + dbf-local also 14/14.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design to move the per-backend #if OPENADS_WITH_X dispatch out of the ~17 ABI
functions into a single registration point (BackendTableOps vtable +
HandleKind-keyed registry). Kills the ~100-hunk merge surface; native path stays
first-class fall-through. Scope: mechanism + SQLite proof, behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
17 sqlite_<op>() free functions copied verbatim from inline ABI blocks,
placed after pad_char_field to ensure that dependency is resolved.
sqlite_table_ops() accessor (static once-init) wires them into a
BackendTableOps struct.  register_builtin_backends() and
backend_table_ops_for() defined in namespace openads::abi immediately
before extern "C" so both state() and sqlite_table_ops() (anonymous
namespace, same TU) are visible.  The 17 ABI functions are NOT modified;
behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline #if OPENADS_WITH_SQLITE blocks in AdsCloseTable,
AdsGotoTop, AdsGotoBottom, AdsSkip with a single registry lookup
via backend_table_ops_for(). Suite: 529 passed / 0 failed / 2 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline #if OPENADS_WITH_SQLITE blocks in AdsAtEOF, AdsAtBOF,
AdsGetNumFields, AdsGetFieldName, AdsGetFieldType, AdsGetFieldLength,
AdsGetFieldDecimals, AdsGetField with registry lookups via
backend_table_ops_for(). Suite: 529 passed / 0 failed / 2 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace inline #if OPENADS_WITH_SQLITE blocks in AdsGetRecordNum,
AdsGetRecordCount, AdsIsRecordDeleted, AdsOpenIndex, AdsIsFound with
registry lookups via backend_table_ops_for(). Suite: 529 passed / 0
failed / 2 skipped. No inline SQLite dispatch remains in any Ads* fn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Avoids leaving the fake ops in the process-global table for later tests in the
same binary (final-review Minor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… registry

Merge the PostgreSQL table driver (pr/openads-plus-postgresql) into the
integration line and route it through the backend-ops registry instead of
the per-function inline #if ladder.

- The 17 ABI table functions stay backend-agnostic: PostgreSQL dispatches
  via the registered postgres_table_ops() (lifted postgres_<op>() bodies),
  exactly mirroring the SQLite lift. No PG inline blocks remain in the ABI
  functions.
- Connection/open, disconnect and index seek/seeklast/closeindex paths are
  added additively alongside the SQLite equivalents (parallel #if guards).
- HandleKind: PostgreSQL handles renumbered to 12/13/14 (SQLite keeps 9/10/11)
  to avoid the enum value clash.
- is_safe_identifier moved to the always-compiled sql_common.cpp so an
  integration build with SQLite + PostgreSQL no longer hits LNK2005.

Verified (MSVC x64, SQLite + PostgreSQL ON together):
- unit suite: original 529 cases all green (behavior-preserving)
- PostgreSQL e2e: 2/2 cases, 41/41 assertions green against a live server

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…gistry

Add the MariaDB/MySQL table driver (from pr/openads-plus-mariadb-backend) to
the integration line and route it through the backend-ops registry — the same
additive pattern proven for PostgreSQL, no git-merge (its base diverges).

- The 17 ABI table functions stay backend-agnostic: MariaDB dispatches via the
  registered maria_table_ops() (lifted maria_<op>() bodies). No Maria inline
  blocks in the ABI functions.
- Connection/open, disconnect and index seek/seeklast/closeindex paths added
  additively alongside the SQLite + PostgreSQL equivalents (parallel #if guards).
- HandleKind: MariaDB handles 15/16/17 (SQLite 9/10/11, PostgreSQL 12/13/14).
- CMake: OPENADS_WITH_MARIADB option + libmariadb wiring; maria_* sources and
  test cases added.

Verified (MSVC x64, SQLite + PostgreSQL + MariaDB ON together):
- unit suite: original 529 cases all green (behavior-preserving)
- MariaDB e2e: 2/2 cases, 45/45 assertions green against a live server
- three backends link into one DLL (coexistence proven)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ps registry

Add the generic ODBC table driver (from pr/openads-plus-odbc) to the
integration line, routed through the backend-ops registry — completing the
SQLite + PostgreSQL + MariaDB + ODBC quartet behind one DLL.

- The 17 ABI table functions stay backend-agnostic: ODBC dispatches via the
  registered odbc_table_ops(). No ODBC inline blocks in those functions.
- Connection/open, disconnect and index seek/seeklast/closeindex paths added
  additively alongside the other backends (parallel #if guards).
- ODBC-only dispatch points the table-ops registry does not cover — AdsCreateIndex61
  (PK-snapshot index via parse_index_expr) and AdsGetRelKeyPos (scrollbar) — are
  grafted inline; these are absent from the pg/maria backends.
- sql_common.{h,cpp} upgraded to the ODBC superset (adds parse_index_expr /
  IndexExprKind); it is already an always-compiled core source.
- HandleKind: ODBC handles 18/19/20. CMake: OPENADS_WITH_ODBC option +
  odbc32/odbc system link; odbc_* sources and three test cases added.

Verified (MSVC x64, SQLite + PostgreSQL + MariaDB + ODBC ON together):
- unit suite: original 529 cases all green (behavior-preserving)
- ODBC e2e: 4/4 cases, 59/59 assertions green vs a throwaway Access .accdb fixture
- four backends link into one DLL (coexistence proven)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… guard

Review follow-up. The PostgreSQL/MariaDB/ODBC backend headers had been
inserted inside the #if defined(OPENADS_WITH_SQLITE) include block, so a
build with SQLITE=OFF but another SQL backend ON would exclude the headers
while still compiling the code that uses those types. Split the includes
into one #if per backend (SQLITE keeps its own uri.h grouping).

Note: SQLITE=OFF is still not a buildable config because the SQL passthrough
feature (already in the base) hard-references SqliteConnection in its
SqlStatement cursor struct without a guard -- a pre-existing coupling, out of
scope here. Supported configs (SQLITE always ON, any subset of PG/MariaDB/
ODBC) build unchanged; the all-backends-ON integration build was reverified
green after this change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the table dispatch mechanism by introducing a pluggable backend-ops registry, moving inline SQL backend dispatch blocks (SQLite, PostgreSQL, MariaDB, and ODBC) into dedicated, backend-specific helper functions. It also introduces openmonitor, a Rust-based terminal and web monitor for the openads_serverd server. The review feedback highlights several critical issues: potential null pointer dereferences in the lifted SQLite helper functions, an unexpanded CMake variable for PostgreSQL include directories, terminal restoration bypass on TUI errors, blocking calls on the Tokio thread pool in the web backend, and potential TUI freezes from blocking TCP connection attempts.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/abi/ace_exports.cpp
Comment on lines +668 to +674
UNSIGNED32 sqlite_goto_top(ADSHANDLE hTable) {
auto* st = get_sqlite_table(hTable);
if (st->conn == nullptr) return fail(openads::AE_INVALID_CONNECTION_HANDLE, "");
auto r = st->conn->goto_top(st);
if (!r) return fail(r.error());
return ok();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The refactored sqlite_goto_top function (and other lifted functions like sqlite_goto_bottom, sqlite_skip, etc.) dereferences st via st->conn without checking if st is nullptr. If get_sqlite_table returns nullptr (e.g., due to an invalid or closed handle), this will cause an immediate segmentation fault / crash. A null check must be added before accessing any members of st.

UNSIGNED32 sqlite_goto_top(ADSHANDLE hTable) {
    auto* st = get_sqlite_table(hTable);
    if (st == nullptr) return fail(openads::AE_TABLE_NOT_OPEN, "table not open");
    if (st->conn == nullptr) return fail(openads::AE_INVALID_CONNECTION_HANDLE, "");
    auto r = st->conn->goto_top(st);
    if (!r) return fail(r.error());
    return ok();
}

Comment thread CMakeLists.txt Outdated
if(NOT _openads_pg_linked)
find_package(PostgreSQL REQUIRED)
add_library(openads_libpq INTERFACE)
target_include_directories(openads_libpq INTERFACE PostgreSQL_INCLUDE_DIRS)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The variable PostgreSQL_INCLUDE_DIRS is passed as a raw string literal to target_include_directories instead of being expanded. This will cause CMake to look for a literal directory named PostgreSQL_INCLUDE_DIRS rather than the actual PostgreSQL include path, leading to compilation failures when find_package(PostgreSQL) is used.

        target_include_directories(openads_libpq INTERFACE "${PostgreSQL_INCLUDE_DIRS}")

Comment thread src/abi/ace_exports.cpp
Comment on lines +319 to +321
std::size_t sqlite_field_index(openads::sql_backend::SqliteTable* st,
UNSIGNED8* pucField) {
if (!st->fields_cached) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The sqlite_field_index helper function dereferences st immediately via !st->fields_cached without verifying if st is nullptr. Since get_sqlite_table can return nullptr, this helper must guard against null pointers to prevent crashes in metadata-related ABI functions.

std::size_t sqlite_field_index(openads::sql_backend::SqliteTable* st,
                               UNSIGNED8* pucField) {
    if (st == nullptr) {
        return std::numeric_limits<std::size_t>::max();
    }
    if (!st->fields_cached) {

Comment on lines +165 to +238
let tick = Duration::from_secs(cfg.interval_secs);
let mut needs_redraw = true;

loop {
if needs_redraw || app.last_refresh.elapsed() >= tick {
app.refresh(cfg);
needs_redraw = true;
}

if needs_redraw {
term.draw(|f| ui(f, app, cfg, args))?;
needs_redraw = false;
}

if event::poll(Duration::from_millis(200))? {
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Char('r') => {
app.refresh(cfg);
app.status_msg = "refreshed".into();
needs_redraw = true;
}
KeyCode::Up => {
app.selected_session = app.selected_session.saturating_sub(1);
needs_redraw = true;
}
KeyCode::Down => {
if !app.inner.http.sessions.is_empty() {
app.selected_session =
(app.selected_session + 1).min(app.inner.http.sessions.len() - 1);
}
needs_redraw = true;
}
KeyCode::Char('g') => {
app.show_graphs = !app.show_graphs;
app.status_msg = if app.show_graphs {
"graphs on".into()
} else {
"graphs off".into()
};
needs_redraw = true;
}
KeyCode::Char('k') => {
if let (Some(base), Some(sess)) =
(&cfg.http, app.inner.http.sessions.get(app.selected_session))
{
let client = HttpClient::new(base);
match client.kill_session(sess.id) {
Ok(()) => {
app.status_msg = format!("killed session {}", sess.id);
app.refresh(cfg);
}
Err(e) => app.status_msg = format!("kill failed: {e}"),
}
needs_redraw = true;
} else {
app.status_msg = "kill needs --http and a selected session".into();
needs_redraw = true;
}
}
_ => {}
}
}
}
}

disable_raw_mode()?;
stdout().execute(LeaveAlternateScreen)?;
ratatui::restore();
Ok(())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

If an error occurs inside the main TUI loop (e.g., during term.draw or event::poll), run_tui will return early via the ? operator. This bypasses the terminal restoration calls (disable_raw_mode, LeaveAlternateScreen, and ratatui::restore), leaving the user's terminal in a broken/unusable state. Wrapping the loop in a closure or block ensures that terminal cleanup is always executed before returning.

    let res = (|| -> Result<()> {
        let tick = Duration::from_secs(cfg.interval_secs);
        let mut needs_redraw = true;

        loop {
            if needs_redraw || app.last_refresh.elapsed() >= tick {
                app.refresh(cfg);
                needs_redraw = true;
            }

            if needs_redraw {
                term.draw(|f| ui(f, app, cfg, args))?;
                needs_redraw = false;
            }

            if event::poll(Duration::from_millis(200))? {
                if let Event::Key(key) = event::read()? {
                    if key.kind != KeyEventKind::Press {
                        continue;
                    }
                    match key.code {
                        KeyCode::Char('q') | KeyCode::Esc => break,
                        KeyCode::Char('r') => {
                            app.refresh(cfg);
                            app.status_msg = "refreshed".into();
                            needs_redraw = true;
                        }
                        KeyCode::Up => {
                            app.selected_session = app.selected_session.saturating_sub(1);
                            needs_redraw = true;
                        }
                        KeyCode::Down => {
                            if !app.inner.http.sessions.is_empty() {
                                app.selected_session =
                                    (app.selected_session + 1).min(app.inner.http.sessions.len() - 1);
                            }
                            needs_redraw = true;
                        }
                        KeyCode::Char('g') => {
                            app.show_graphs = !app.show_graphs;
                            app.status_msg = if app.show_graphs {
                                "graphs on".into()
                            } else {
                                "graphs off".into()
                            };
                            needs_redraw = true;
                        }
                        KeyCode::Char('k') => {
                            if let (Some(base), Some(sess)) = 
                                (&cfg.http, app.inner.http.sessions.get(app.selected_session))
                            { 
                                let client = HttpClient::new(base);
                                match client.kill_session(sess.id) {
                                    Ok(()) => {
                                        app.status_msg = format!("killed session {}", sess.id);
                                        app.refresh(cfg);
                                    }
                                    Err(e) => app.status_msg = format!("kill failed: {}", e),
                                }
                                needs_redraw = true;
                            } else {
                                app.status_msg = "kill needs --http and a selected session".into();
                                needs_redraw = true;
                            }
                        }
                        _ => {}
                    }
                }
            }
        }
        Ok(())
    })();

    disable_raw_mode()?;
    stdout().execute(LeaveAlternateScreen)?;
    ratatui::restore();
    res

Comment on lines +97 to +108
match HttpClient::new(&base).kill_session(id) {
Ok(()) => {
if let Ok(mut state) = st.poll.write() {
state.refresh(&st.cfg);
if let Ok(mut out) = st.snap.write() {
*out = state.to_snapshot(&st.cfg);
}
}
StatusCode::OK.into_response()
}
Err(e) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Calling the blocking HttpClient::kill_session directly inside the asynchronous api_kill_session handler blocks the Tokio worker thread. This can degrade the performance of the entire web server. It should be wrapped in tokio::task::spawn_blocking to run on a dedicated thread pool.

    let base_clone = base.clone();
    let kill_res = tokio::task::spawn_blocking(move || {
        HttpClient::new(&base_clone).kill_session(id)
    })
    .await;

    match kill_res {
        Ok(Ok(())) => {
            if let Ok(mut state) = st.poll.write() {
                state.refresh(&st.cfg);
                if let Ok(mut out) = st.snap.write() {
                    *out = state.to_snapshot(&st.cfg);
                }
            }
            StatusCode::OK.into_response()
        }
        Ok(Err(e)) => (StatusCode::BAD_GATEWAY, e.to_string()).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }

Comment on lines +192 to +195
pub fn fetch_mg_snapshot(host: &str, port: u16) -> Result<MgSnapshot> {
let addr = format!("{host}:{port}");
let mut stream = TcpStream::connect(&addr).with_context(|| format!("connect {addr}"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

TcpStream::connect blocks indefinitely (or up to the OS default timeout of 20+ seconds) if the host is unreachable or firewalled. Since this is a TUI application that runs on a single thread, this will freeze the interface. Using TcpStream::connect_timeout with a short timeout (e.g., 3 seconds) prevents long freezes.

Suggested change
pub fn fetch_mg_snapshot(host: &str, port: u16) -> Result<MgSnapshot> {
let addr = format!("{host}:{port}");
let mut stream = TcpStream::connect(&addr).with_context(|| format!("connect {addr}"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;
pub fn fetch_mg_snapshot(host: &str, port: u16) -> Result<MgSnapshot> {
use std::net::ToSocketAddrs;
let addr = format!("{host}:{port}");
let socket_addr = addr
.to_socket_addrs()
.context("resolve address")?
.next()
.ok_or_else(|| anyhow!("failed to resolve address"))?;
let mut stream = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(3))
.with_context(|| format!("connect {addr}"))?;
stream.set_read_timeout(Some(Duration::from_secs(5)))?;

Gemini review on FiveTechSoft#31. In the find_package(PostgreSQL) fallback path the
include-dirs variable was passed as a literal token instead of being expanded.
Use \. (The explicit path / OPENADS_LIBPQ_* path that
the e2e build uses was unaffected, which is why it built green.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Admnwk

Admnwk commented Jun 21, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Going through the findings:

CMakeLists.txt:221 (high) — fixed in 248d2a1: the include-dirs variable is now expanded (${PostgreSQL_INCLUDE_DIRS}). It only affected the find_package(PostgreSQL) fallback; the explicit OPENADS_LIBPQ_* path used by the e2e build was unaffected, which is why it built green.

ace_exports.cpp:674 / :321 (sqlite_goto_top, sqlite_field_index, etc.) — not a reachable crash. These lifted helpers are reached only through the backend-ops registry: an ABI function calls ops->X(h) only after backend_table_ops_for(h) resolved the handle's HandleKind to SqliteTable. get_sqlite_table(h) then looks the same handle up under that exact kind, so it cannot return null on this path — the old if (auto* st = get_sqlite_table(h)) guard was doing the kind-filter that the registry now performs up front. A null here would mean the registry returned ops for a kind whose object pointer is null (an internal invariant break), not a reachable input. Glad to add a defensive guard if you'd prefer belt-and-suspenders.

openmonitor/*.rs (main.rs:238, web.rs:108, wire.rs:195) — these are in the standalone openmonitor Rust tool, not the backend-ops registry change this PR is about; it rode in via the branch base. Valid points, but they belong to that tool's own track rather than this refactor.

Admnwk and others added 4 commits June 21, 2026 17:54
Keep integration/openadsplus current with upstream. Clean auto-merge (src/CMakeLists.txt union of backend options + x86 .def docs). x64 build + core unit suite (49 cases / 447 assertions) green on the merged tree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three strict-flag failures on the openadsplus integration branch:

- sqlite_backend.cpp: exec_pragma/escape_pragma_key are used only under OPENADS_WITH_SQLCIPHER; guard their definitions with the same #if so the default (SQLCipher OFF) build does not trip clang -Wunused-function.

- sqlite_connection.cpp: cast the loop index to std::size_t at the vector subscripts to avoid -Wsign-conversion (int -> size_type).

- ace_exports.cpp: wrap the trig_* helpers (return std::string/std::vector/TrigError_) in an extern "C++" block so MSVC /W4 /WX does not raise C4190 for C-linkage functions returning C++-incompatible types.

Behavior-preserving; verified with a strict MSVC x64 build (/W4 /WX) of openads_core (56/56, exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Unit tests used std::vector / std::min / std::array but relied on transitive includes that MSVC and libstdc++ happen to provide; clang/libc++ (and a strict CI) do not. Add the missing <vector>/<algorithm>/<array> headers (include-what-you-use).

Verified by a full gcc/libstdc++ build of openads_unit_tests (191/191, exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant