Skip to content

Commit 3d69e3c

Browse files
committed
cron: Add 'tickless' mode
- time: Add function to round timestamp and timespans to milliseconds
1 parent e51e9fe commit 3d69e3c

File tree

3 files changed

+132
-33
lines changed

3 files changed

+132
-33
lines changed

src/cron.q

+113-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Cron Job Scheduler
2-
// Copyright (c) 2017 - 2019 Sport Trades Ltd
2+
// Copyright (c) 2017 - 2020 Sport Trades Ltd, 2021 Jaskirat Rajasansir
33

44
// Documentation: https://github.com/BuaBook/kdb-common/wiki/cron.q
55

@@ -16,10 +16,10 @@
1616
/ @see .cron.status
1717
.cron.cfg.logStatus:1b;
1818

19-
/ The supported run type for the cron system
20-
.cron.cfg.runners:()!();
21-
.cron.cfg.runners[`once]:`.cron.i.runOnce;
22-
.cron.cfg.runners[`repeat]:`.cron.i.runRepeat;
19+
/ The mode of operaton for the cron system. There are 2 supported modes:
20+
/ * ticking: Traditional timer system with the timer function running on a frequent interval
21+
/ * tickless: New approach to only 'tick' the timer when the next job is due to run. Can reduce process load when infrequent jobs are run
22+
.cron.cfg.mode:`ticking;
2323

2424

2525
/ Unique job ID for each cron job added
@@ -34,57 +34,73 @@
3434
`.cron.status upsert @[first .cron.status; `result; :; (::)];
3535

3636

37-
/ NOTE: The initialistion function will not overwrite .z.ts if it is already set.
37+
/ The supported run type for the cron system
38+
.cron.runners:(`symbol$())!`symbol$();
39+
.cron.runners[`once]: `.cron.i.runOnce;
40+
.cron.runners[`repeat]:`.cron.i.runRepeat;
41+
42+
/ The supported tick modes for the cron system
43+
.cron.supportedModes:(`symbol$())!`symbol$();
44+
.cron.supportedModes[`ticking]: `.cron.mode.ticking;
45+
.cron.supportedModes[`tickless]:`.cron.mode.tickless;
46+
47+
/ The maximum supported timer interval as a timespan
48+
.cron.maxTimerAsTimespan:.convert.msToTimespan 0Wi - 1;
49+
50+
/ One millisecond as a timespan (to not require calculation each time)
51+
.cron.oneMsAsTimespan:.convert.msToTimespan 1;
52+
53+
54+
/ NOTE: If '.z.ts' is defined at initialisation, the function will short-circuit and not configure the library
3855
.cron.init:{
3956
if[.ns.isSet `.z.ts;
4057
.log.if.warn "Timer function is already set. Cron will not override automatically";
4158
:(::);
4259
];
4360

4461
set[`.z.ts; .cron.ts];
45-
.cron.enable[];
62+
.cron.changeMode .cron.cfg.mode;
4663

4764
if[not `.cron.cleanStatus in exec func from .cron.jobs;
4865
.cron.addRepeatForeverJob[`.cron.cleanStatus; (::); `timestamp$.time.today[]+1; 1D];
4966
];
5067
};
5168

5269

53-
/ Starts the kdb timer to activate the cron system
54-
.cron.enable:{
55-
if[0 < system"t";
56-
:(::);
70+
/ Changes between the supported cron timer modes
71+
/ @param mode (Symbol) The cron timer mode to use
72+
/ @throws InvalidCronModeException If the mode is not one of the supported modes
73+
/ @see .cron.supportedModes
74+
.cron.changeMode:{[mode]
75+
if[not mode in key .cron.supportedModes;
76+
.log.if.error "Cron timer mode is invalid. Must be one of: ",.convert.listToString key .cron.supportedModes;
77+
'"InvalidCronModeException";
5778
];
5879

59-
.log.if.info "Enabling cron job scheduler [ Timer Interval: ",string[.cron.cfg.timerInterval]," ms ]";
60-
61-
.util.system "t ",string .cron.cfg.timerInterval;
62-
};
80+
.cron.cfg.mode:mode;
81+
.cron.supportedModes[.cron.cfg.mode][];
82+
};
6383

64-
/ Disableds the kdb timer to deactivate the cron system
84+
/ Disables the kdb timer to deactivate the cron system
6585
.cron.disable:{
66-
if[0 = system"t";
67-
:(::);
68-
];
69-
7086
.log.if.info "Disabling cron job scheduler";
7187
.log.if.warn " No scheduled jobs will be executed until cron is enabled again";
7288

73-
.util.system "t 0";
89+
system "t 0";
7490
};
7591

7692
/ General job addition function. Adds a job to the cron system for execution
7793
/ @param func (Symbol) Symbol reference to the function to execute
7894
/ @param args () Any arguments that are required to execute the function. Pass generic null (::) for no arguments
79-
/ @param runType (Symbol) The type of cron job to add. See .cron.cfg.runners
80-
/ @param startTime (Timestamp) The first time the job will be run
81-
/ @param endTime (Timestamp) The time to finish a repeating job executing. Pass null (0Np) to repeat forever or for one time jobs
95+
/ @param runType (Symbol) The type of cron job to add. See .cron.runners
96+
/ @param startTime (Timestamp) The first time the job will be run. NOTE: Timestamp will be rounded to the nearest millisecond
97+
/ @param endTime (Timestamp) The time to finish a repeating job executing. Pass null (0Np) to repeat forever or for one time jobs. NOTE: Timestamp will be rounded to the nearest millisecond
8298
/ @param interval (Timespan) The interval at which repeating jobs should recur. Pass null (0Nn) for one time jobs
8399
/ @returns (Long) The ID of the new cron job
84100
/ @throws InvalidCronJobIntervalException If the interval specified is smaller than the cron interval
85101
/ @throws FunctionDoesNotExistFunction If the function for the cron job does not exist
86102
/ @throws ReferenceIsNotAFunctionException If the symbol reference for the function is not actually a function
87-
/ @throws InvalidCronRunTypeException If the run type specified is not present in .cron.cfg.runners
103+
/ @throws InvalidCronRunTypeException If the run type specified is not present in .cron.runners
88104
/ @throws InvalidCronJobTimeException If the start time specified is before the current time or the end time is before the start time
89105
.cron.add:{[func;args;runType;startTime;endTime;interval]
90106
if[not .ns.isSet func;
@@ -97,12 +113,20 @@
97113
'"ReferenceIsNotAFunctionException";
98114
];
99115

100-
if[not runType in key .cron.cfg.runners;
101-
.log.if.error "Invalid cron run type. Expecting one of: ",.convert.listToString key .cron.cfg.runners;
116+
if[not runType in key .cron.runners;
117+
.log.if.error "Invalid cron run type. Expecting one of: ",.convert.listToString key .cron.runners;
102118
'"InvalidCronRunTypeException";
103119
];
104120

105-
if[startTime < .time.today[]+`second$.time.nowAsTime[];
121+
if[not all .type.isTimestamp each (startTime; endTime);
122+
.log.if.error "Invalid start time or end time. Must be a timestamp";
123+
'"InvalidCronJobTimeException";
124+
];
125+
126+
startTime:.time.roundTimestampToMs startTime;
127+
endTime:.time.roundTimestampToMs endTime;
128+
129+
if[startTime < .time.nowAsMsRoundedTimestamp[];
106130
.log.if.error "Cron job start time is in the past. Cannot add job";
107131
'"InvalidCronJobTimeException";
108132
];
@@ -111,9 +135,9 @@
111135
.log.if.error "Cron job end time specified is before the start time. Cannot add job";
112136
'"InvalidCronJobTimeException";
113137
];
114-
115-
if[not[.util.isEmpty interval] & .cron.cfg.timerInterval > .convert.timespanToMs interval;
116-
.log.if.error "Cron job repeat interval is shorter than the cron timer interval. Cannot add job";
138+
139+
if[(`ticking = .cron.cfg.mode) & not[.util.isEmpty interval] & .cron.cfg.timerInterval > .convert.timespanToMs interval;
140+
.log.if.error "Cron job repeat interval is shorter than the cron timer interval (ticking). Cannot add job";
117141
'"InvalidCronJobIntervalException";
118142
];
119143

@@ -122,6 +146,10 @@
122146

123147
`.cron.jobs upsert (jobId;func;args;runType;startTime;endTime;interval;startTime);
124148

149+
if[`tickless = .cron.cfg.mode;
150+
.cron.i.setNextTick[];
151+
];
152+
125153
:jobId;
126154
};
127155

@@ -164,6 +192,10 @@
164192
];
165193

166194
update nextRunTime:0Wp from `.cron.jobs where id = jobId;
195+
196+
if[`tickless = .cron.cfg.mode;
197+
.cron.i.setNextTick[];
198+
];
167199
};
168200

169201
/ Removes all entries from .cron.status and all jobs that will not run again. By default this is run at
@@ -177,7 +209,11 @@
177209
/ The main cron function that is bound to .z.ts as part of the initialisation
178210
.cron.ts:{
179211
toRun:0!select id, runType from .cron.jobs where nextRunTime <= .time.now[];
180-
{ get[.cron.cfg.runners x`runType] x`id } each toRun;
212+
.cron.runners[toRun`runType] @' toRun`id;
213+
214+
if[`tickless = .cron.cfg.mode;
215+
.cron.i.setNextTick[];
216+
];
181217
};
182218

183219
/ Execution function for jobs that only run once
@@ -245,3 +281,48 @@
245281
:status;
246282
};
247283

284+
/ Updates the 'tickless' timer tick based on the next run time. If no more cron jobs are scheduled to run, the timer will be disabled
285+
/ until a new job is added
286+
/ @see .cron.jobs
287+
/ @see .cron.oneMsAsTimespan
288+
/ @see .cron.maxTimerAsTimespan
289+
.cron.i.setNextTick:{
290+
nextRun:exec min nextRunTime from .cron.jobs;
291+
292+
if[.type.isInfinite nextRun;
293+
.log.if.trace "No active cron jobs scheduled. Disabling system timer";
294+
system "t 0";
295+
:(::);
296+
];
297+
298+
/ Always make sure the next timer tick:
299+
/ * Is not 0 (so accidentally disabled)
300+
/ * Is not greater than max integer - 1
301+
timer:.cron.maxTimerAsTimespan & .cron.oneMsAsTimespan | nextRun - .time.roundTimestampToMs .time.now[];
302+
timerMs:.convert.timespanToMs timer;
303+
304+
if[timerMs = system "t";
305+
:(::);
306+
];
307+
308+
system "t ",string timerMs;
309+
310+
.log.if.trace "Tickless cron timer updated [ Next Run: ",string[timer]," (",string[timerMs]," ms) ]";
311+
};
312+
313+
314+
/ Enables the 'ticking' cron mode
315+
/ NOTE: Does not validate the configured ticking mode
316+
/ @see .cron.cfg.timerInterval
317+
.cron.mode.ticking:{
318+
.log.if.info "Enabling cron job scheduler [ Mode: Ticking ] [ Timer Interval: ",string[.cron.cfg.timerInterval]," ms ]";
319+
system "t ",string .cron.cfg.timerInterval;
320+
};
321+
322+
/ Enables the 'tickless' cron mode
323+
/ NOTE: Does not validate the configured ticking mode
324+
/ @see .cron.i.setNextTick
325+
.cron.mode.tickless:{
326+
.log.if.info "Enabling cron job scheduler [ Mode: Tickless ]";
327+
.cron.i.setNextTick[];
328+
};

src/time.util.q

+18
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,26 @@
3535
:except[;".:"] string[.time.today[]],"_",string[ddmmss],"_",string millis;
3636
};
3737

38+
/ @returns (Timestamp) Current time as timestamp but rounded to the nearest millisecond
39+
.time.nowAsMsRoundedTimestamp:{
40+
:.time.today[] + .time.nowAsTime[];
41+
};
42+
3843
/ @returns (String) A file name friendly representation of the current date. Format is 'yyyymmdd'
3944
/ @see .time.today[]
4045
.time.todayForFileName:{
4146
:except[;"."] string .time.today[];
4247
};
48+
49+
/ Rounds nanosecond precision timestamps and timespans to milliseconds
50+
.time.roundTimestampToMs:.time.roundTimespanToMs:{
51+
if[not any .type[`isTimestamp`isTimespan] @\: x;
52+
'"IllegalArgumentException";
53+
];
54+
55+
if[.type.isInfinite x;
56+
:x;
57+
];
58+
59+
:.Q.t[abs type x]$1000000 * (`long$x) div 1000000;
60+
};

src/type.q

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
/ All infinite values
77
/ @see .type.isInfinite
8-
.type.const.infinites:raze (::;neg)@\:(0Wh;0Wi;0Wj;0We;0Wf;0Wp;0Wm;0Wd;0Wz;0Nn;0Wu;0Wv;0Wt);
8+
.type.const.infinites:raze (::;neg)@\:(0Wh;0Wi;0Wj;0We;0Wf;0Wp;0Wm;0Wd;0Wz;0Wn;0Wu;0Wv;0Wt);
99

1010
/ Mapping of type name based on index in the list (matching .Q.t behaviour)
1111
.type.const.types:`mixedList`boolean`guid``byte`short`integer`long`real`float`character`symbol`timestamp`month`date`datetime`timespan`minute`second`time;

0 commit comments

Comments
 (0)