Skip to content

Feature serverselect#110

Open
ascottDI wants to merge 10 commits into
mainfrom
feature-serverselect
Open

Feature serverselect#110
ascottDI wants to merge 10 commits into
mainfrom
feature-serverselect

Conversation

@ascottDI

Copy link
Copy Markdown

di.serverselect — TorQ Modularisation PR

Summary

Extracts the server-selection logic from TorQ's .gw namespace (gatewaylib.q and gateway.q) into a standalone kdb-x module: di.serverselect. The module maintains a registered pool of backend servers and selects from them by servertype or attribute requirements. It satisfies the di.* module contract: dependency injection via init, a clean exported API, and no hard module dependencies beyond an injected logger.


Background

TorQ's gateway uses a .gw namespace to track connected backend servers and route queries to them. Server registration, active-flag management, and selection logic were entangled with the gateway's query execution and connection-handling code. This PR extracts the server-selection layer — the pool of registered servers and the logic to pick from it — as an independently loadable and testable unit.

This PR is part of the broader TorQ → kdb-x modularisation effort. The extracted module removes the dependency on TorQ's global process framework and can be used in any gateway or proxy that needs to maintain a backend server pool.


Changes

New files

File Description
di/serverselect/serverselect.q Core implementation — init, addserverfull, addserverattr, addserver, setserveractive, getserverstable, addserversfromtable, getservers, selector, getserverbytype, gethandlebytype, gethpbytype, getserverids and internal helpers
di/serverselect/init.q Module entry point — loads serverselect.q and declares the export list
di/serverselect/test.csv k4unit unit-test suite (128 assertions)
di/serverselect/integration.q End-to-end integration test (76 assertions)
di/serverselect/serverselect.md Module README

Differences from TorQ original

Aspect TorQ .gw di.serverselect
Logging Hard-coded .lg.o/.lg.e calls Injected log dependency, required via init (no fallback)
Server state Global .gw.servers table .z.m.servers module-local mutable state
Error handling Mix of silent failures and hard exits Every error path logged then signalled with a di.serverselect: prefix
Module contract None kdb-x use singleton, init[deps] pattern, export: list
Attribute matching Inlined in query dispatch Extracted as attributematch (internal); exposed via the getservers attribmatch column
Bulk registration .servers.SERVERS hard-coded lookup Generic addserversfromtable[proctypes;conntable] accepting any connection table

Logging contract (injected dependency)

Logging is an injected dependency, wired via init — there is no default logger and the module does not load kx.log itself; initialising the logging framework is the job of the start-up script or the user. init must be called before any other function.

  • init[deps] takes a single dict carrying the required log dependency (plus any future optional config). It errors immediately (plain signal) if deps is not a dict, is missing `log, or log is not a dict exposing `info`warn`error.
  • normlog normalises the injected logger to a binary `info`warn`error!{[c;m]} dict — each function takes a context symbol c and a message string m. It accepts either:
    • a whole kx.log instance ((use\kx.log)[`createLog][]) — detected by its getlvl/sinks/fmtskeys; its monadic functions are wrapped to{[c;m]}, folding context in as a "ctx: msg"` prefix; or
    • a bespoke binary `info`warn`error!({[c;m]};{[c;m]};{[c;m]}) dict — passed through unchanged.
  • Every call site uses the binary form .z.m.log[\info][`ctx;"msg"](which maps 1:1 with TorQ's.lg.o[`ctx;"msg"]`).
  • Errors are routed through an internal raiseerror[ctx;msg] helper that logs via .z.m.log[\error]**then** signals'"di.serverselect: ",ctx,": ",msg`, so every failure is observable in the log as well as thrown.

Exported API

srvsel:use`di.serverselect

/ wire the required log dependency first (pass the whole kx.log instance; normlog wraps it)
srvsel.init[enlist[`log]!enlist (use`kx.log)[`createLog][]]

srvsel.addserverfull[h;pname;st;hp;att]   / register a server with full details
srvsel.addserverattr[h;st;att]            / register a server with servertype and attributes
srvsel.addserver[h;st]                    / register a server with no attributes
srvsel.setserveractive[h;active]          / mark a server active (1b) or inactive (0b)
srvsel.getserverstable[]                  / return the full registered server table
srvsel.addserversfromtable[types;ctab]    / bulk-register from a connection table
srvsel.getservers[nameortype;lookups;req] / look up active servers with attribute scoring
srvsel.selector[servertable;selection]    / pick one row using roundrobin / any / last strategy
srvsel.getserverbytype[ptype;col;sel]     / return one column value for a servertype
srvsel.gethandlebytype[ptype;sel]         / convenience projection: return handle
srvsel.gethpbytype[ptype;sel]             / convenience projection: return hpup
srvsel.getserverids[att]                  / return server IDs by servertype list or attribute dict

Hardening and fixes

  • updatestats keyed on serverid, not handle. handle is not unique (the table is keyed on serverid, and a freed handle can be reused after reconnect). Keying stats updates on the unique serverid prevents a single selection from skewing hits/lastp across every server that happens to share a handle. Covered by a dedicated regression test.
  • Input validation on the public mutators, routed through raiseerror so the failure is logged: addserverfull/setserveractive check handle (int) and servertype/active-flag types; addserversfromtable checks the connection table has the required w/proctype/attributes columns.
  • getservers validates nameortype — it must be `servertype or `procname (when lookups is not `); any other value errors rather than silently falling through to a procname lookup.
  • selector empty-table behaviour documented — it returns a null-valued row; the *bytype helpers guard against this and return () when no active server matches.

Test coverage

Two suites, both green.

Unit tests — test.csv (k4unit), 128 assertions

Area Coverage
init — dependency injection accepts a valid log dep; rejects non-dict deps, missing log, non-dict log, log dict missing a key; di.serverselect: error prefix
init — injected logger used a capturing binary logger receives info messages on registration and error messages on failure paths
registration addserver/addserverattr/addserverfull — row counts, handles, servertypes, procname/hpup population, serverid autoincrement, type-error rejection
setserveractive active flag toggled; missing handle is a no-op; type-error rejection
updatestats a selection updates only the chosen serverid, even when a handle is shared
getservers column schema incl. attribmatch; servertype/procname filters; null lookups; per-attribute match scoring; invalid nameortype rejected
selector roundrobin/last/any strategies; single-row and empty-table behaviour; unknown strategy rejected
getserverbytype / gethandlebytype / gethpbytype column value returned; () for unknown type; round-robin rotation; hits increment
getserverids — symbol path IDs for registered types; rejects null / unregistered / all-inactive
getserverids — attribute path date/sym match; cross and independent matching; servertype-scoped dict; besteffort strict mode
addserversfromtable bulk register; skips already-active handles; proctype filter; `ALL; optional procname/hpup columns; missing-column rejection

Integration test — integration.q, 76 assertions

Drives every exported function through a realistic gateway lifecycle (register → activate/deactivate → query → select → bulk-register → error handling → init re-wiring with a capturing logger), with a PASS/FAIL summary and a non-zero exit code equal to the number of failures.

Running the tests

export QPATH=/path/to/kx/mod:/path/to/kdbx-modules
/ unit tests
k4unit:use`di.k4unit;
k4unit.moduletest`di.serverselect;
/ integration test
QPATH=/path/to/kx/mod:/path/to/kdbx-modules q integration.q

Integration notes

  • di.serverselect has no hard module dependencies. The injected log is required — init throws if it is not provided, and must be called before any other function.
  • The caller wires kx.log by passing the whole createLog[] instance (srvsel.init[enlist[\log]!enlist (use`kx.log)[`createLog][]]); normlogdetects and adapts it. A stripped 3-key dict of kx.log's monadic functions must **not** be passed (it would bypass detection and fail with'rank`).
  • addserversfromtable accepts a TorQ .servers.SERVERS-style table directly: columns w (handle), proctype, attributes are required; procname and hpup are optional.
  • getserverids supports the full TorQ gateway attribute-matching contract: cross-product matching (default), independent matching, per-servertype scoping, and besteffort mode. Its result is consumed by the gateway via inter/: + first each (and raze for emptiness checks), which is robust to its per-path return shape.
  • All error messages are prefixed di.serverselect: to identify the source in stack traces.

Test results screenshot

image image

Comment thread di/serverselect/integration.q Outdated
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/serverselect.q
@DI-Software-Engineering

Copy link
Copy Markdown

DIReview Summary

1 critical | 7 warning(s) | 0 suggestion(s)

⚠️ Spec check skipped — tracker lookup failed (NO_REF_FOUND). Standards axis only.

Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
Comment thread di/serverselect/integration.q
@DI-Software-Engineering

Copy link
Copy Markdown

DIReview Summary

1 critical | 2 warning(s) | 0 suggestion(s)

⚠️ Spec check skipped — tracker lookup failed (NO_REF_FOUND). Standards axis only.

Comment thread di/serverselect/serverselect.q
@DI-Software-Engineering

Copy link
Copy Markdown

DIReview Summary

1 critical | 0 warning(s) | 0 suggestion(s)

⚠️ Spec check skipped — tracker lookup failed (NO_REF_FOUND). Standards axis only.

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.

2 participants