-
Notifications
You must be signed in to change notification settings - Fork 7
Feature server #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ascottDI
wants to merge
10
commits into
main
Choose a base branch
from
feature-server
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Feature server #120
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
031a21b
initial creation of servers module plus refinements
d15e9ac
Adding in temporary intergration tests until actual intergration avai…
efb35be
aligned with di.toml module. Increased robustness of init guards, cut…
eb7c189
pulling in modules that are merged to main
40f8035
refactor to fit merged modules rather than mocked. Handlers module st…
cd42728
cleaning up orphaned code
7e42fb2
Bringing servers up to date with main
395861f
adding in the version control
5d4d0da
Update version convention, di.server name changed to di.servers, rest…
alowrydi af726cf
following automated reviewer comments
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| 0.1.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| / connection management and handle-by-type lookup for the modular torq world. | ||
| \l ::servers.q | ||
| / module version, read from the VERSION file rather than hardcoded, so a release bump touches one | ||
| / plain-text file. read module-relative at load (`:::` resolves to di/servers), and BEFORE the export | ||
| / line since export:([...]) evaluates each name. NB `version` stays in the export: di.depcheck | ||
| / resolves a dependency's version from the export dict. | ||
| / trim so a trailing newline/CRLF cannot pad the semver; fail loud with a clear message if VERSION is | ||
| / missing/unreadable/empty (it is a required module file - better than a raw OS error, a silent empty | ||
| / value, or a misleading 0.0.0 that would corrupt depcheck's version comparison). | ||
| version:@[{trim first read0 x};`:::VERSION;{'"di.servers: VERSION file missing or unreadable"}]; | ||
| if[0=count version;'"di.servers: VERSION file is empty"]; | ||
| export:([init;startup;getservers;gethandlebytype;waitfortype;getapimeta;version]) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| # di.servers | ||
|
|
||
| Connection management and handle-by-type lookup for the modular TorQ world — the `di.*` | ||
| analogue of TorQ's `.servers` (`code/handlers/trackservers.q` + `servers.q`), scoped down | ||
| for v1: no discovery service, no password/access-list files, no non-TorQ process tracking, | ||
| no FinSpace. `process.csv` here is a static **phone book** (who to *dial*), **not** an | ||
| identity source — self-identity comes from config, injected by `di.torq`. | ||
|
|
||
| FRAMEWORK-tier module: no hard `di.*` dependencies; `log`, `timer` and `handlers` are all | ||
| **injected** (all required, no fallback). | ||
|
|
||
| ## init and config | ||
|
|
||
| Standard **one-arg `init[deps]`**: `di.torq` merges this process's resolved config slice into | ||
| the same `deps` dict it passes the injectables in, so `deps` carries both the injectable | ||
| dependencies **and** the config keys. `init` wires the deps, records self-identity, and | ||
| installs two one-time process-global side effects — a `.z.pc` cleanup handler and a 10s retry | ||
| timer job. It is **idempotent** (guarded by an internal `registered` flag): `di.torq` calls | ||
| it once per process, but a second call refreshes the dep refs without re-registering (a | ||
| duplicate `di.timer.addjob` id would throw). `init` does **not** open connections. | ||
|
|
||
| `deps` keys: | ||
|
|
||
| | key | kind | meaning | | ||
| |---|---|---| | ||
| | `log` | injectable | binary `` `info`warn`error `` `{[c;m]}` logger dict — di.log's `logdict``log` satisfies this directly (it carries all six levels; the extra `trace`/`debug`/`fatal` are ignored) | | ||
| | `timer` | injectable | the di.timer export dict; di.servers calls `` timer[`addjob][`custom] `` — the 6-arg `{[id;func;params;period;mode;opts]}` variant (`addjob` is a variant dict of `custom`/`default`/`simple`) | | ||
| | `handlers` | injectable | di.handlers contract; `register[event;phase;nm;pri;func]` | | ||
| | `proctype`/`procname` | config | this process's own identity (required); used to exclude self from `process.csv` | | ||
| | `connections` | config | proctypes this process should dial (symbols, or strings from a `.toml` cascade — normalised). Optional; default = none | | ||
| | `processcsv` | config | **path** to `process.csv`; supplied by di.torq. Optional; required only once `connections` is non-empty | | ||
|
|
||
| ```q | ||
| svc:use`di.servers | ||
| svc.init[deps] / deps = injectables + config, assembled by di.torq | ||
| svc.startup[] / open the configured connections (reads init config) | ||
| h:svc.gethandlebytype[`hdb;`any] | ||
| h "1+1" | ||
| ``` | ||
|
|
||
| ## Exported functions | ||
|
|
||
| | Function | Signature | Description | | ||
| |---|---|---| | ||
| | `init` | `init[deps]` | Wire deps + config, record identity, install the `.z.pc` handler + retry job. Idempotent. | | ||
| | `startup` | `startup[]` | Read `process.csv` (`processcsv`), drop self, connect to each row whose proctype is in `connections`. A failed connection is logged (not raised) and left as `w:0Ni` for `retry`. No-op if no connections configured. **Idempotent**: skips procs already tracked in `SERVERS`, so a repeat call (or a grown `process.csv`) adds only new rows — never a duplicate or a leaked second handle. `process.csv` must be the strict v1 4-column `host,port,proctype,procname` layout — a reordered or wider header is **rejected loudly** (the reader is positional, so it would otherwise misparse silently). **Self-exclusion is an exact `(proctype;procname)` match against the identity from config**, so a `process.csv` that disagrees would leave this process dialling itself; any row carrying this process's `procname` **that survives the connections filter (i.e. one this process would otherwise dial)** is therefore skipped with a warning naming both proctypes. A drifted row whose proctype isn't a connection type is already filtered out and poses no self-connection risk, so it is not warned about. | | ||
| | `getservers` | ``getservers[proctype]`` | Live (`w` non-null) `SERVERS` rows. Accepts a **symbol**, a **symbol list** (rows for any of them), or `` ` `` for **every** proctype — the contract legacy TorQ's `.servers.getservers` (`trackservers.q:75`) and sibling `di.serverselect.getservers` both implement, so a consumer written against either works here unchanged. | | ||
| | `gethandlebytype` | `gethandlebytype[proctype;selection]` | One live handle via `` `any``/`roundrobin`/`last``; `0Ni` if none. Bumps usage stats. | | ||
| | `waitfortype` | `waitfortype[proctype;timeoutms;pollms]` | Block until a live connection exists or timeout; `1b`/`0b`. Caller decides if a timeout is fatal. `startup` must have run first. | | ||
| | `getapimeta` | `getapimeta[]` | This module's api metadata, one row per **callable** API function (`init`/`getapimeta`/`version` plumbing omitted), for `di.torq` to register with `di.api`. | | ||
| | `version` | `version` | The module's semver string (`"0.1.0"`) — metadata, not a function. Read at load from the plain-text `VERSION` file in the module folder; read by `di.depcheck` to satisfy other modules' declared minimum-version requirements. | | ||
|
|
||
| Export is deliberately conservative — only functions `di.torq` or a consumer actually calls | ||
| (so `di.api` lists exactly these), plus the `version` metadata string. The rest are **internal**: `retry` (the scheduled | ||
| `serversretry` job — passed to the timer *by value* at init, so it needs no export; it first | ||
| runs `cleanup` to sweep ungracefully-vanished handles, then reopens every dead handle), | ||
| `cleanup`, `formathp`, `opencon`, `readprocesscsv`, `retryrows`, `selector`, `updatestats`, | ||
| `signalfound`, `raiseerror`; plus state (`SERVERS`, `self`, `registered`, `HOPENTIMEOUT`, | ||
| `connections`, `processcsv`). | ||
|
|
||
| ## The `SERVERS` table | ||
|
|
||
| ```q | ||
| SERVERS:([]procname:`symbol$();proctype:`symbol$();hpup:`symbol$();w:`int$();hits:`int$();startp:`timestamp$();lastp:`timestamp$();endp:`timestamp$()) | ||
| ``` | ||
|
|
||
| A direct analogue of legacy TorQ's `.servers.SERVERS`: `w` is the live handle (`0Ni` when | ||
| disconnected), `hits`/`lastp` drive handle selection, `startp`/`endp` track lifecycle. | ||
|
|
||
| ## `.z.pc` registration via di.handlers | ||
|
|
||
| `.z.pc` (connection closed) is a **simple/observer** event in di.handlers — side-effect only, | ||
| fan-out — so di.servers registers its cleanup callback through the injected `handlers` | ||
| dependency rather than assigning `.z.pc` directly: | ||
|
|
||
| ```q | ||
| (handlers[`register])[`.z.pc;`;`servers;0j;pcfunc] | ||
| ``` | ||
|
|
||
| `register`'s signature is `register[event;phase;nm;pri;func]`; for a simple event the `phase` | ||
| must be `` ` `` (null) — di.handlers rejects a non-null phase on an observer event. This lets | ||
| di.servers' disconnect hook coexist with every other `.z.pc` registrant in the same | ||
| priority-ordered fan-out. | ||
|
|
||
| ## Conventions (learnings from di.config) | ||
|
|
||
| - **One-arg `init[deps]`** with config folded into `deps` (the project convention; matches | ||
| `di.eodtime`'s optional-config-in-deps pattern), not a two-arg `init[config;deps]`. | ||
| - **Three-flat-var logging** — `.z.m.loginfo`/`.z.m.logwarn`/`.z.m.logerr`, matching | ||
| `consistency.md`, `di.compression` and `di.config`. (The project hasn't globally frozen this | ||
| vs. the single-dict form — flag before changing.) | ||
| - **`raiseerror` (log-then-signal)** for all post-init domain errors (`selector` unknown | ||
| selection, missing or malformed `process.csv`). `init`'s own dependency validation is the one | ||
| exception (plain `'` — no logger yet). | ||
| - **`getapimeta`** exported; a test asserts it documents exactly the module's *callable* | ||
| exports — `init`/`getapimeta`/`version` are plumbing/metadata (di.torq calls or reads them by | ||
| convention) and are deliberately omitted from the registry rows, matching di.toml and the skill | ||
| convention. | ||
| - **`version` export** — a bare exported semver string (`"0.1.0"`, numeric `major.minor.patch`), | ||
| read by di.depcheck to satisfy other modules' declared minimum-version requirements. The source | ||
| of truth is **`version.txt`** in the module folder: `init.q` reads it module-relative at load | ||
| (`:::version.txt`) and it takes **priority** over the compiled-in fallback in `init.q`; a missing | ||
| or empty `version.txt` falls back to that default. Bump the release version by editing | ||
| `version.txt` alone. | ||
| - **Env-free** — di.servers reads no environment variable; the `process.csv` path arrives via | ||
| `config`processcsv` (di.torq resolves it), holding di.config's env-free boundary. | ||
|
|
||
| ## Open items / not yet done | ||
|
|
||
| - **Live-peer integration tests are in place** (`test.q` + `test.csv`, 37 checks), and now wire the | ||
| **real merged `di.timer` and `di.log`** — only `di.handlers` (not yet merged) is mocked. They spawn | ||
| a genuinely separate `q` peer (a self-connect returns pseudo-handle `0`, not a real socket) and | ||
| cover: `startup` connecting to a live peer and logging a failed dial while excluding self, | ||
| `gethandlebytype` returning a live remote handle (`2=h"1+1"`), the retry cycle recovering an | ||
| ungraceful kill (`cleanup`+reopen), and `waitfortype` connected-vs-timeout — plus init validation, | ||
| dep-wiring, idempotency, input validation, and `getapimeta`. `init` schedules `serversretry` in the | ||
| real `di.timer` (asserted via `` timer.getalljobs[] ``); because `retry`/`cleanup` are internal, the | ||
| retry cycle is driven by invoking the exact func di.servers handed the timer (`` firejob `` reads it | ||
| back from `` getalljobs[] `` — the actually-wired path), not a direct export. Idempotent re-init is | ||
| a genuine test here: the real timer's `` addjob[`custom] `` throws on a duplicate id, so a | ||
| non-idempotent `init` would fail outright. A final check re-inits against di.log's real `logdict` to | ||
| prove the injected-log contract holds end-to-end. | ||
| - **`di.handlers` not in kdbx-modules yet**, so only that injected contract is mocked. The handlers | ||
| mock uses the real `register[event;phase;nm;pri;func]` shape from `handlers.q`; the `.z.pc` observer | ||
| path is therefore exercised only via the explicit `retry`→`cleanup` sweep, not a live auto-fired | ||
| `.z.pc` (which real di.handlers would install). | ||
| - **`config`processcsv` and the assembled `connections` list** depend on di.torq's config | ||
| wiring — coordinate when di.torq's servers dep is built. | ||
| - Scoped-out (v1): discovery service, password/access-list files, non-TorQ tracking, and | ||
| `tcps`/`unix` socket types. Only `tcp` is supported; `formathp` builds a `tcp` handle with no | ||
| socket-type arg — a future `SOCKETTYPE` config reintroduces that (with a test) when needed. | ||
|
|
||
| ## Tests | ||
|
|
||
| Run in a fresh q session (spawns and kills a real peer process; don't interleave with other | ||
| modules' tests). Needs `QHOME` set (the peer is launched via `$QHOME/bin/q`) and `di.os` on | ||
| `QPATH` (the harness uses `os.abspath` to load `test.q`): | ||
|
|
||
| ```q | ||
| k4unit:use`di.k4unit | ||
| k4unit.moduletest`di.servers | ||
| ``` |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.