Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions di/tplog/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.1.0
5 changes: 5 additions & 0 deletions di/tplog/deps.q
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/ hard module dependencies and their minimum versions, validated by di.depcheck.
/ di.tplog is self-contained - it uses no other di.* module via `use` (lifecycle and byte-scan
/ recovery are built on base q only). its one runtime dependency, log, is injected via init as a
/ dictionary of functions and is validated by di.depcheck's core-contract check, not declared here.
deps:(`$())!();
77 changes: 7 additions & 70 deletions di/tplog/init.q
Original file line number Diff line number Diff line change
@@ -1,72 +1,9 @@
/ header to build deserialisable msg
header:8#-8!(`upd;`trade;());
/ first part of tp update msg
updmsg:`char$10#8_-8!(`upd;`trade;());
/ size of default chunk to read (10MB)
chunk:10*1024*1024;
/ don't let single read exceed this
maxchunk:8*chunk;
/ di.tplog - tickerplant log lifecycle (open/write/roll/replay/replayupto/logname) plus corruption
/ check/repair. self-contained (no hard `use` deps); log is injected via init. see tplog.md
\l ::tplog.q

check:{[logfile;lastmsgtoreplay]
/ logfile (symbol) is the handle to the logsfile
/ lastmsgtoreplay (long) is index position of the last message to be replayed from the log
/ check if the logfile is corrupt
loginfo:-11!(-2;logfile);
:$[1 = count loginfo;
/ - the log file is good so return the good log file handle
:logfile;
loginfo[0] <= lastmsgtoreplay + 1;
:logfile;
repair[logfile]
]
};

repair:{[logfile]
/ - append ".good" to the "good" log file
goodlog: `$ string[logfile],".good";
/ - create file and open handle to it
goodlogh: hopen goodlog set ();
/ - loop through the file in chunks
repairover[logfile;goodlogh] over `start`size!(0j;chunk);
/ - return goodlog
goodlog
};

repairover:{[logfile;goodlogh;d]
/ logfile (symbol) is the handle to the logsfile
/ goodlogh (int) is the handle to the "good" log file
/ d (dictionary) has two keys start and size, the point to start reading from and size of chunk to read
/ read <size> bytes from <start>
x:read1 logfile,d`start`size;
/ find the start points of upd messages
u: ss[`char$x;updmsg];
/ nothing in this block
if[not count u;
/ EOF - we're done
if[hcount[logfile] <= sum d`start`size;:d];
/ move on <size> bytes
:@[d;`start;+;d`size]];
/ split bytes into msgs
m: u _ x;
/ message sizes as bytes
mz: 0x0 vs' `int$ 8 + ms: count each m;
/ set msg size at correct part of hdr
hd: @[header;7 6 5 4;:;] each mz;
/ try and deserialize each msg
g: @[(1b;)@-9!;;(0b;)@] each hd,'m;
/ write good msgs to the "good" log
goodlogh g[;1] where k:g[;0];
/ saw msg(s) but couldn't read
if[not any k;
/ read as much as we dare, give up
if[maxchunk <= d`size;
:@[d;`start`size;:;(sum d`start`size;chunk)]];
/ read a bigger chunk
:@[d;`size;*;2]];
/ move to the end of the last good msg
ns: d[`start] + sums[ms] last where k;
:@[d;`start`size;:;(ns;chunk)];
};

export:([check;repair])
/ module version, read from the VERSION file before the export line evaluates each name.
/ trim so a trailing newline / CRLF cannot pad the semver di.depcheck compares
version:trim first read0`:::VERSION

export:([init;logname;open;write;roll;replay;replayupto;check;repair;getapimeta;version])
58 changes: 38 additions & 20 deletions di/tplog/test.csv
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
action,ms,bytes,lang,code,repeat,minver,comment
before,0,0,q,tplog:use`di.tplog,1,,Initialize module
before,0,0,q,os:use`di.os,1,,Initialize module
/ note location of the test.q file might be different, built under assumption using di module file system
before,0,0,q,"system ""l "", os.abspath[""di/tplog/test.q""]",1,1,load additional testing functions / dependencies
run,0,0,q,testrepairandreplay[],1,1,
run,0,0,q,testrepairrecoversmessages[],1,1,
run,0,0,q,testrepaircreatesgoodfile[],1,1,
run,0,0,q,testcheckvalidlog[],1,1,
run,0,0,q,testcheckcorruptsufficientmessages[],1,1,
run,0,0,q,testrepaircreatesgoodfile[],1,1,
run,0,0,q,testrepairrecoversmessages[],1,1,
run,0,0,q,testchecktriggersrepair[],1,1,
run,0,0,q,testrepairgarbageatend[],1,1,
run,0,0,q,testmultiplecorruptsections[],1,1,
run,0,0,q,testcompletelycorruptlog[],1,1,
run,0,0,q,testemptylog[],1,1,
run,0,0,q,testrepairandreplay[],1,1,
run,0,0,q,testlargefilehandling[],1,1,
run,0,0,q,testrepaircreatesgoodfile[],1,1,
run,0,0,q,testsequentialoperations[],1,1,
before,0,0,q,tp:use`di.tplog,1,,load the module under test
before,0,0,q,os:use`di.os,1,,os module for portable path resolution
before,0,0,q,"system ""l "",os.abspath[""di/tplog/test.q""]",1,,load fixture helpers (defines root upd + trade + helpers)
before,0,0,q,tp.init[enlist[`log]!enlist capturelog[]],1,,init with a capturing logger (the required log dependency)
before,0,0,q,setupfixture[],1,,create the temp fixture root
comment,,,,,,,module metadata - version and getapimeta
true,0,0,q,10h=type tp`version,1,1,version is a string
true,0,0,q,0<count tp`version,1,1,version is non-empty
true,0,0,q,`name`public`descrip`params`return~cols tp[`getapimeta][],1,1,getapimeta rows carry the registry columns
true,0,0,q,(asc (key tp) except `init`getapimeta`version)~asc exec name from tp[`getapimeta][],1,1,getapimeta documents exactly the callable exports
true,0,0,q,not any `init`getapimeta`version in exec name from tp[`getapimeta][],1,1,plumbing and version are not registered in the api
comment,,,,,,,init dependency validation
fail,0,0,q,tp.init[(::)],1,1,init rejects a non-dict deps
fail,0,0,q,tp.init[(enlist`x)!enlist 1],1,1,init rejects deps without a log key
fail,0,0,q,tp.init[enlist[`log]!enlist 1],1,1,init rejects a non-dict log value
fail,0,0,q,tp.init[enlist[`log]!enlist (enlist`info)!enlist{[c;m]}],1,1,init rejects a log dict missing warn/error keys
comment,,,,,,,logname
true,0,0,q,testlogname[],1,1,logname builds the <dir>/tp<date> handle
comment,,,,,,,open / write / roll lifecycle
true,0,0,q,testopenfresh[],1,1,a freshly created log opens with count 0
true,0,0,q,testwritereopen[],1,1,write two then reopen replays both through the root upd
true,0,0,q,testroll[],1,1,roll closes the handle and creates the next day's log
fail,0,0,q,testopenfailsfast[],1,1,open fails fast on a corrupt log
comment,,,,,,,replay / replayupto (repair-aware)
true,0,0,q,testreplayrepairs[],1,1,replay repairs a corrupt log and recovers messages
true,0,0,q,testreplaynodouble[],1,1,replay processes each recovered message exactly once
true,0,0,q,testreplayupto[],1,1,replayupto replays only the first n messages
comment,,,,,,,check / repair corruption utilities
true,0,0,q,testcheckclean[],1,1,check returns a clean log unchanged
true,0,0,q,testcheckcorruptwarns[],1,1,check repairs a corrupt log and logs a warning
true,0,0,q,testrepaircreatesgood[],1,1,repair writes a .good file
comment,,,,,,,public-function input validation
fail,0,0,q,tp[`logname][`notastring;2026.08.13],1,1,logname rejects a non-string dir
fail,0,0,q,tp[`replay]["notasymbol"],1,1,replay rejects a non-symbol logfile
fail,0,0,q,tp[`check]["notasymbol"],1,1,check rejects a non-symbol logfile
fail,0,0,q,tp[`repair]["notasymbol"],1,1,repair rejects a non-symbol logfile
fail,0,0,q,tp[`replayupto][tp[`logname]["/x";2026.08.13];`notanint],1,1,replayupto rejects a non-integral n
after,0,0,q,teardownfixture[],1,,remove the temp fixture root
Loading