Feature tickerplant - #125
Conversation
…plant are merged to the same branch
DIReview Summary1 critical | 2 warning(s) | 0 suggestion(s)
|
DIReview Summary0 critical | 3 warning(s) | 0 suggestion(s)
|
… and re-init Publish subdetails and tablelist at root so di.subscriptions can attach. A standard TorQ tickerplant never had these - only chainedtp.q and segmentedtickerplant.q define subdetails - so this is a deliberate capability addition rather than restored parity, recorded in the design-divergences section of tickerplant.md. - keep the tp log path in .z.m.logpath; .z.m.logfile stays the handle, so the path is no longer overwritten and can be reported to subscribers - advance .z.m.i in zero-latency mode, as chainedtp.q's tickpub does; without it every subscriber was told to replay nothing - report the PUBLISHED watermark i in logfilelist, not the logged total j: in batch mode a row logged but not yet flushed is still buffered and goes out at the next tick, so j would replay it AND deliver it again. TorQ sends .u.i for the same reason (chainedtp.q) and kdb+tick's r.q replays with .u`i - seed runtime state only on a fresh init, so a re-init cannot rewind the trading date, zero the counts a subscriber replays against, or reopen the log; table materialisation follows the same rule, since re-running the schema over a live tickerplant discarded buffered rows - track per-table published rowcounts for the protocol's rowcounts field - fail loud on a missing, unreadable or empty VERSION - guard the cold-path exports with requireinit; upd stays unguarded as the per-message hot path, and a test pins which side each is on Adds test_integration.csv: a real di.subscriptions driven against a real di.tickerplant over IPC, covering both batch and zero-latency modes plus the VERSION guards. It needs di.tplog with init (feature-tplog) and di.subscriptions on the same branch, as a5ac34d notes for the existing dependent tests.
| }; | ||
|
|
||
| teardown:{[] | ||
| / release everything init installed process-wide: the root subscription protocol and the timer job. |
There was a problem hiding this comment.
pubsub.subscribe returns a bare error symbol when no tables match, but the partial-match branch checks type first r — when r is a symbol atom, first of a symbol atom is the atom itself (not its first element), so -11h=type first r is 1b for BOTH the all-fail case and a partial-match case where the first element of the returned pair happens to be a symbol. The all-fail guard (-11h=type r) is evaluated first, so the all-fail path is handled correctly, but if pubsub.subscribe ever returns a pair whose first element is a symbol atom (e.g. an error symbol paired with partial results), the partial-match branch would silently log and continue rather than raise. More concretely: after the all-fail guard the only shapes remaining are (tables;schemas) and (errmsg;(tables;schemas)). In the partial case first r is an error symbol atom, so -11h=type first r is correct — but the code relies on first of a 2-element list returning the first element, which is fine. The real bug is that pairs:flip $[partial;last r;r] uses last r when partial, but pubsub.subscribe on a partial match returns (errmsg;(tables;schemas)) — so last r is (tables;schemas), and flip of that is (tables list; schemas list), which is correct. However when all tables match, r is (tables;schemas) directly, and flip r yields the same shape. This path is actually consistent. The real issue: in the all-none case the guard correctly signals, but for ZERO matched tables where pubsub.subscribe instead returned a (tables;schemas) pair with empty lists, pairs[;0] would be an empty list and nms!0^.z.m.rowcounts nms would be an empty dict — silently returning a dict that looks like success with no tables. Verify that pubsub.subscribe always returns a bare symbol (not an empty pair) when nothing matches.
| if[.z.m.logfile>0i;.z.m.logfile enlist (`upd;t;x);.z.m.j+:1]; | ||
| }; | ||
|
|
||
| logfilelist:{[] |
There was a problem hiding this comment.
logfilelist checks if[null .z.m.logpath;:()] but openlog sets .z.m.logpath:`` (the null symbol) when logging is disabled (0=count .z.m.logdir). The nullcheck on a symbol atom is correct per q semantics. However on the very first call tosubdetailsbefore anyinit(i.e. beforeopenloghas run),.z.m.logpathdoes not exist yet and the protected-apply ininitialised[]guards against that for the public entry points — butlogfilelistis an internal helper called fromsubdetailswhich itself callsrequireinit. So the ordering is safe only because requireinitruns first. This is fine as written, but iflogfilelistis ever called from a path that skipsrequireinit, it will throw '.m.di.0tickerplant.logpath`. No fix needed unless the call graph changes, but worth noting the implicit coupling.
| if[`schemas in key `.m.di.0tickerplant; | ||
| tp[`teardown][]; | ||
| @[{if[.m.di.0tickerplant.logfile>0i;hclose .m.di.0tickerplant.logfile]};::;{[e] :(::)}]; | ||
| ![`.m.di.0tickerplant;();0b;`schemas`scheduled]]; |
There was a problem hiding this comment.
resetmodule calls tp[\teardown][]and then directly manipulates.m.di.0tickerplantstate with@[{if[.m.di.0tickerplant.logfile>0i;hclose .m.di.0tickerplant.logfile]};::;{[e] :(::)}]followed by![`.m.di.0tickerplant;();0b;`schemas`scheduled]. But teardownalready sets.z.m.scheduled:0b(which is.m.di.0tickerplant.scheduled), so the manual delete of scheduledis redundant. More critically,teardowndoes NOT close the log handle — it deliberately leaves module state intact per the design. The explicithcloseimmediately afterteardownis therefore the only place the log is closed during reset. Ifteardownthrows (e.g. because it internally errors before thescheduled:0bwrite), thehcloseis still attempted via the protected apply, which is correct. However, deletingschemasfrom.m.di.0tickerplantafter ateardownthat has already returned meansinitialised[]will correctly probe false — this is the intended mechanism. The sequence is fragile: iftickerplant.qever adds a new name thatinitialised[]probes,resetmodule silently stops working. The test comment acknowledges this (initialised[] probes), but the fix — exporting a test-only reset or making initialised[]probe a dedicated flag — is documented as a deliberate decision. Not blocking, but the brittleness is real: adding a second probe variable toinitialised[]intickerplant.q` would silently break the test suite.
| .z.m.loginfo[`openlog;"logging to ",string l]; | ||
| :h; | ||
| }; | ||
|
|
There was a problem hiding this comment.
publishbuffer computes .z.m.rowcounts:.z.m.rowcounts+.z.m.tabs!count each . .z.m.tabsbefore callingpubsub.pubclear, which publishes and then empties each table. If pubsub.pubclearthrows (e.g. a subscriber handle is broken and the publish errors out internally), the row counts have already been incremented but the rows may not have been cleared from the buffer or fully published. On the next tick,pubclearwould attempt to publish the same rows again androwcountswould be incremented a second time, producing double-countedrowcountsand potentially duplicate delivery. The safer order is to incrementrowcountsafterpubclearsucceeds. Consider wrapping the whole sequence or at minimum moving therowcountsupdate afterpubclear`.
| before,0,0,q,H1 (`feed;4 5),1,1,"two more updates: logged, still buffered, NOT yet published" | ||
| before,0,0,q,"ATSUB:H1""counts[]""",1,1,capture: the counts a subscriber arriving now is answered from | ||
| before,0,0,q,TPD:ATSUB`d,1,1,capture: the trading date the tickerplant reports | ||
| before,0,0,q,R1:sub.subscribe[H1;`;`;1b;1b],1,1,"THE CALL UNDER TEST - ` for all tables, so tablelist is exercised too; define the schemas at root and replay the log" |
There was a problem hiding this comment.
The readport helper uses a while loop with system"sleep 0.05" — a busy-wait that the project style guide explicitly prohibits (do not use the do, while, and for functions). While this is test/integration code rather than production code, it violates the documented project convention. Replace with a recursive converge or a timed \ iteration, or at minimum acknowledge the exception in a comment.
DIReview Summary0 critical | 5 warning(s) | 0 suggestion(s)
|
di.tickerplant
Summary
Adds
di.tickerplant, the core tick-capture process for the modular TorQ world: it receives updatesfrom feeds, stamps them, writes them to a tickerplant log for recovery, and publishes them to
subscribers, rolling the log at end of day. It is the modular replacement for TorQ's
code/processes/tickerplant.q.It orchestrates three hard dependencies —
di.pubsub(subscribe/publish),di.eodtime(roll timing)and
di.tplog(log check/repair) — with an injected logger and timer.Motivation
TorQ's
tickerplant.qhand-rolls the whole capture loop —.z.*handling, the.upublish/subscribemachinery, log writing and rolling, and end-of-day scheduling — in one process file. The modular
framework already provides those pieces as standalone modules (
di.pubsub,di.eodtime,di.tplog,di.timer), so this extraction re-expresses the tickerplant as an orchestration layer over them:capture, stamp, log, publish, roll — delegating pub/sub, roll timing and log recovery to the modules
that own them.
Design
Root tables and the upd contract
The captured tables live at root, not in
.z.m: a tickerplant owns its tables, feeds insert intothem, and
di.pubsubreads them by name, so they cannot be module-local. This is the one deliberateroot-state exception; all other mutable state is module-local.
di.torqwires the process's rootupdtotickerplant.updso feeds publish to it.Capture modes
updinserts into the root table; a timer job publishes the accumulated rowsevery
batchperiodand clears them.updpublishes each update immediately and does not buffer.Both modes stamp the update (prepending a data timestamp unless one is already present) and log every
message.
End of day
The roll fires when the current time passes
di.eodtime's next roll timestamp, checked on everyupdand every timer tick.
endofdayflushes the buffer, notifies subscribers, rolls the log to the nextday, and refreshes the roll time and data-timestamp offset from
di.eodtime.Use of
di.tplogdi.tplogis used only for check/repair on recovery: whenopenlogfinds a pre-existing log it runsit through
tplog.check, repairing a corrupt one. The tickerplant opens for append and rolls thelog itself, rather than using
di.tplog'sopen/roll, because those replay the log throughupd—which a tickerplant must not do to its own log. (This module was updated to the new
di.tplogcontract: it now calls
tplog.initduring its owninit, andtplog.checkis 1-arg.)No
di.handlersdependencySubscriber-disconnect cleanup is handled by
di.pubsub's own.z.pc, so this module takes no handlerdependency.
di.pubsubshould migrate todi.handlersso.z.*is not assigned outside the centralregistry — tracked separately, out of scope here.
Dependencies
di.pubsubuse)di.eodtimeuse)di.tploguse)log`info`warn`errordict of{[ctx;msg]}functionstimerdi.timer's exports (must exposeaddjob)Hard deps are declared in
deps.q; the injectedlogandtimerare validated bydi.depcheck'score-contract check, not declared there.
initvalidates every dependency strictly and signalsimmediately if any is missing or malformed.
Public API
init[deps]`g#onsym), open today's log, and schedule the batch/roll timer job. Idempotent.upd[table;data]subscribe[tables;filters]di.pubsub).endofday[]getcounts[]`i`j`d— messages published, messages logged, trading date.gettables[]version"0.1.0").updvalidates its arguments and routes failures through a log-then-signal helper.getapimeta[]exposes the callable API for central registration with
di.api; the framework plumbing (init,getapimeta,version) is intentionally excluded.initalso accepts optional config keys:batch(1b),batchperiod(timespan),logdir(string,""disables logging),logname(prefix, default"tp"),subtables(symbol list), and thedi.eodtimekeys (rolltimezone/datatimezone/rolltimeoffset) forwarded verbatim.Testing
test.csv/test.q(k4unit), 18 checks, run against the realdi.pubsub,di.eodtime,di.tploganddi.timer— no dependencies are mocked. The timer is used withoutinit(so no live.z.ts; its job is exercised throughendofday), and a capturing logger is shared across the modulesso their output is assertable.
Coverage: the metadata/version contract; strict
initdependency validation (afailrow per guard);init materialising the root tables and scheduling the timer job; batch and zero-latency
upd;endofdayflushing and rolling;updinput validation; and the twodi.tplogintegration points — atickerplant-written log replaying through
di.tplog, and rolling into a corrupt log repairing it viatplog.check.Because two tests replay/repair through
di.tplog, the suite passes oncedi.tploganddi.tickerplantare present together (as they merge) — it is not runnable against an olderdi.tplog.Files
The module version lives in a plain-text
VERSIONfile, read ininit.q(version:trim first read0\:::VERSION) and exported fordi.depcheck, matching the convention used bydi.servers,di.clienttrackinganddi.tplog`.