1/*
2 * Copyright (c) 2009-2021, Redis Ltd.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * * Redistributions of source code must retain the above copyright notice,
9 * this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * * Neither the name of Redis nor the names of its contributors may be used
14 * to endorse or promote products derived from this software without
15 * specific prior written permission.
16 *
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 * POSSIBILITY OF SUCH DAMAGE.
28 */
29
30#include "server.h"
31#include "script.h"
32#include "cluster.h"
33
34scriptFlag scripts_flags_def[] = {
35 {.flag = SCRIPT_FLAG_NO_WRITES, .str = "no-writes"},
36 {.flag = SCRIPT_FLAG_ALLOW_OOM, .str = "allow-oom"},
37 {.flag = SCRIPT_FLAG_ALLOW_STALE, .str = "allow-stale"},
38 {.flag = SCRIPT_FLAG_NO_CLUSTER, .str = "no-cluster"},
39 {.flag = SCRIPT_FLAG_ALLOW_CROSS_SLOT, .str = "allow-cross-slot-keys"},
40 {.flag = 0, .str = NULL}, /* flags array end */
41};
42
43/* On script invocation, holding the current run context */
44static scriptRunCtx *curr_run_ctx = NULL;
45
46static void exitScriptTimedoutMode(scriptRunCtx *run_ctx) {
47 serverAssert(run_ctx == curr_run_ctx);
48 serverAssert(scriptIsTimedout());
49 run_ctx->flags &= ~SCRIPT_TIMEDOUT;
50 blockingOperationEnds();
51 /* if we are a replica and we have an active master, set it for continue processing */
52 if (server.masterhost && server.master) queueClientForReprocessing(server.master);
53}
54
55static void enterScriptTimedoutMode(scriptRunCtx *run_ctx) {
56 serverAssert(run_ctx == curr_run_ctx);
57 serverAssert(!scriptIsTimedout());
58 /* Mark script as timedout */
59 run_ctx->flags |= SCRIPT_TIMEDOUT;
60 blockingOperationStarts();
61}
62
63int scriptIsTimedout() {
64 return scriptIsRunning() && (curr_run_ctx->flags & SCRIPT_TIMEDOUT);
65}
66
67client* scriptGetClient() {
68 serverAssert(scriptIsRunning());
69 return curr_run_ctx->c;
70}
71
72client* scriptGetCaller() {
73 serverAssert(scriptIsRunning());
74 return curr_run_ctx->original_client;
75}
76
77/* interrupt function for scripts, should be call
78 * from time to time to reply some special command (like ping)
79 * and also check if the run should be terminated. */
80int scriptInterrupt(scriptRunCtx *run_ctx) {
81 if (run_ctx->flags & SCRIPT_TIMEDOUT) {
82 /* script already timedout
83 we just need to precess some events and return */
84 processEventsWhileBlocked();
85 return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;
86 }
87
88 long long elapsed = elapsedMs(run_ctx->start_time);
89 if (elapsed < server.busy_reply_threshold) {
90 return SCRIPT_CONTINUE;
91 }
92
93 serverLog(LL_WARNING,
94 "Slow script detected: still in execution after %lld milliseconds. "
95 "You can try killing the script using the %s command. Script name is: %s.",
96 elapsed, (run_ctx->flags & SCRIPT_EVAL_MODE) ? "SCRIPT KILL" : "FUNCTION KILL", run_ctx->funcname);
97
98 enterScriptTimedoutMode(run_ctx);
99 /* Once the script timeouts we reenter the event loop to permit others
100 * some commands execution. For this reason
101 * we need to mask the client executing the script from the event loop.
102 * If we don't do that the client may disconnect and could no longer be
103 * here when the EVAL command will return. */
104 protectClient(run_ctx->original_client);
105
106 processEventsWhileBlocked();
107
108 return (run_ctx->flags & SCRIPT_KILLED) ? SCRIPT_KILL : SCRIPT_CONTINUE;
109}
110
111uint64_t scriptFlagsToCmdFlags(uint64_t cmd_flags, uint64_t script_flags) {
112 /* If the script declared flags, clear the ones from the command and use the ones it declared.*/
113 cmd_flags &= ~(CMD_STALE | CMD_DENYOOM | CMD_WRITE);
114
115 /* NO_WRITES implies ALLOW_OOM */
116 if (!(script_flags & (SCRIPT_FLAG_ALLOW_OOM | SCRIPT_FLAG_NO_WRITES)))
117 cmd_flags |= CMD_DENYOOM;
118 if (!(script_flags & SCRIPT_FLAG_NO_WRITES))
119 cmd_flags |= CMD_WRITE;
120 if (script_flags & SCRIPT_FLAG_ALLOW_STALE)
121 cmd_flags |= CMD_STALE;
122
123 /* In addition the MAY_REPLICATE flag is set for these commands, but
124 * if we have flags we know if it's gonna do any writes or not. */
125 cmd_flags &= ~CMD_MAY_REPLICATE;
126
127 return cmd_flags;
128}
129
130/* Prepare the given run ctx for execution */
131int scriptPrepareForRun(scriptRunCtx *run_ctx, client *engine_client, client *caller, const char *funcname, uint64_t script_flags, int ro) {
132 serverAssert(!curr_run_ctx);
133
134 int running_stale = server.masterhost &&
135 server.repl_state != REPL_STATE_CONNECTED &&
136 server.repl_serve_stale_data == 0;
137 int obey_client = mustObeyClient(caller);
138
139 if (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE)) {
140 if ((script_flags & SCRIPT_FLAG_NO_CLUSTER) && server.cluster_enabled) {
141 addReplyError(caller, "Can not run script on cluster, 'no-cluster' flag is set.");
142 return C_ERR;
143 }
144
145 if (running_stale && !(script_flags & SCRIPT_FLAG_ALLOW_STALE)) {
146 addReplyError(caller, "-MASTERDOWN Link with MASTER is down, "
147 "replica-serve-stale-data is set to 'no' "
148 "and 'allow-stale' flag is not set on the script.");
149 return C_ERR;
150 }
151
152 if (!(script_flags & SCRIPT_FLAG_NO_WRITES)) {
153 /* Script may perform writes we need to verify:
154 * 1. we are not a readonly replica
155 * 2. no disk error detected
156 * 3. command is not `fcall_ro`/`eval[sha]_ro` */
157 if (server.masterhost && server.repl_slave_ro && !obey_client) {
158 addReplyError(caller, "-READONLY Can not run script with write flag on readonly replica");
159 return C_ERR;
160 }
161
162 /* Deny writes if we're unale to persist. */
163 int deny_write_type = writeCommandsDeniedByDiskError();
164 if (deny_write_type != DISK_ERROR_TYPE_NONE && !obey_client) {
165 if (deny_write_type == DISK_ERROR_TYPE_RDB)
166 addReplyError(caller, "-MISCONF Redis is configured to save RDB snapshots, "
167 "but it's currently unable to persist to disk. "
168 "Writable scripts are blocked. Use 'no-writes' flag for read only scripts.");
169 else
170 addReplyErrorFormat(caller, "-MISCONF Redis is configured to persist data to AOF, "
171 "but it's currently unable to persist to disk. "
172 "Writable scripts are blocked. Use 'no-writes' flag for read only scripts. "
173 "AOF error: %s", strerror(server.aof_last_write_errno));
174 return C_ERR;
175 }
176
177 if (ro) {
178 addReplyError(caller, "Can not execute a script with write flag using *_ro command.");
179 return C_ERR;
180 }
181
182 /* Don't accept write commands if there are not enough good slaves and
183 * user configured the min-slaves-to-write option. */
184 if (server.masterhost == NULL &&
185 server.repl_min_slaves_max_lag &&
186 server.repl_min_slaves_to_write &&
187 server.repl_good_slaves_count < server.repl_min_slaves_to_write)
188 {
189 addReplyErrorObject(caller, shared.noreplicaserr);
190 return C_ERR;
191 }
192 }
193
194 /* Check OOM state. the no-writes flag imply allow-oom. we tested it
195 * after the no-write error, so no need to mention it in the error reply. */
196 if (server.pre_command_oom_state && server.maxmemory &&
197 !(script_flags & (SCRIPT_FLAG_ALLOW_OOM|SCRIPT_FLAG_NO_WRITES)))
198 {
199 addReplyError(caller, "-OOM allow-oom flag is not set on the script, "
200 "can not run it when used memory > 'maxmemory'");
201 return C_ERR;
202 }
203
204 } else {
205 /* Special handling for backwards compatibility (no shebang eval[sha]) mode */
206 if (running_stale) {
207 addReplyErrorObject(caller, shared.masterdownerr);
208 return C_ERR;
209 }
210 }
211
212 run_ctx->c = engine_client;
213 run_ctx->original_client = caller;
214 run_ctx->funcname = funcname;
215
216 client *script_client = run_ctx->c;
217 client *curr_client = run_ctx->original_client;
218 server.script_caller = curr_client;
219
220 /* Select the right DB in the context of the Lua client */
221 selectDb(script_client, curr_client->db->id);
222 script_client->resp = 2; /* Default is RESP2, scripts can change it. */
223
224 /* If we are in MULTI context, flag Lua client as CLIENT_MULTI. */
225 if (curr_client->flags & CLIENT_MULTI) {
226 script_client->flags |= CLIENT_MULTI;
227 }
228
229 run_ctx->start_time = getMonotonicUs();
230 run_ctx->snapshot_time = mstime();
231
232 run_ctx->flags = 0;
233 run_ctx->repl_flags = PROPAGATE_AOF | PROPAGATE_REPL;
234
235 if (ro || (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) && (script_flags & SCRIPT_FLAG_NO_WRITES))) {
236 /* On fcall_ro or on functions that do not have the 'write'
237 * flag, we will not allow write commands. */
238 run_ctx->flags |= SCRIPT_READ_ONLY;
239 }
240 if (!(script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) && (script_flags & SCRIPT_FLAG_ALLOW_OOM)) {
241 /* Note: we don't need to test the no-writes flag here and set this run_ctx flag,
242 * since only write commands can are deny-oom. */
243 run_ctx->flags |= SCRIPT_ALLOW_OOM;
244 }
245
246 if ((script_flags & SCRIPT_FLAG_EVAL_COMPAT_MODE) || (script_flags & SCRIPT_FLAG_ALLOW_CROSS_SLOT)) {
247 run_ctx->flags |= SCRIPT_ALLOW_CROSS_SLOT;
248 }
249
250 /* set the curr_run_ctx so we can use it to kill the script if needed */
251 curr_run_ctx = run_ctx;
252
253 return C_OK;
254}
255
256/* Reset the given run ctx after execution */
257void scriptResetRun(scriptRunCtx *run_ctx) {
258 serverAssert(curr_run_ctx);
259
260 /* After the script done, remove the MULTI state. */
261 run_ctx->c->flags &= ~CLIENT_MULTI;
262
263 server.script_caller = NULL;
264
265 if (scriptIsTimedout()) {
266 exitScriptTimedoutMode(run_ctx);
267 /* Restore the client that was protected when the script timeout
268 * was detected. */
269 unprotectClient(run_ctx->original_client);
270 }
271
272 preventCommandPropagation(run_ctx->original_client);
273
274 /* unset curr_run_ctx so we will know there is no running script */
275 curr_run_ctx = NULL;
276}
277
278/* return true if a script is currently running */
279int scriptIsRunning() {
280 return curr_run_ctx != NULL;
281}
282
283const char* scriptCurrFunction() {
284 serverAssert(scriptIsRunning());
285 return curr_run_ctx->funcname;
286}
287
288int scriptIsEval() {
289 serverAssert(scriptIsRunning());
290 return curr_run_ctx->flags & SCRIPT_EVAL_MODE;
291}
292
293/* Kill the current running script */
294void scriptKill(client *c, int is_eval) {
295 if (!curr_run_ctx) {
296 addReplyError(c, "-NOTBUSY No scripts in execution right now.");
297 return;
298 }
299 if (mustObeyClient(curr_run_ctx->original_client)) {
300 addReplyError(c,
301 "-UNKILLABLE The busy script was sent by a master instance in the context of replication and cannot be killed.");
302 }
303 if (curr_run_ctx->flags & SCRIPT_WRITE_DIRTY) {
304 addReplyError(c,
305 "-UNKILLABLE Sorry the script already executed write "
306 "commands against the dataset. You can either wait the "
307 "script termination or kill the server in a hard way "
308 "using the SHUTDOWN NOSAVE command.");
309 return;
310 }
311 if (is_eval && !(curr_run_ctx->flags & SCRIPT_EVAL_MODE)) {
312 /* Kill a function with 'SCRIPT KILL' is not allow */
313 addReplyErrorObject(c, shared.slowscripterr);
314 return;
315 }
316 if (!is_eval && (curr_run_ctx->flags & SCRIPT_EVAL_MODE)) {
317 /* Kill an eval with 'FUNCTION KILL' is not allow */
318 addReplyErrorObject(c, shared.slowevalerr);
319 return;
320 }
321 curr_run_ctx->flags |= SCRIPT_KILLED;
322 addReply(c, shared.ok);
323}
324
325static int scriptVerifyCommandArity(struct redisCommand *cmd, int argc, sds *err) {
326 if (!cmd || ((cmd->arity > 0 && cmd->arity != argc) || (argc < -cmd->arity))) {
327 if (cmd)
328 *err = sdsnew("Wrong number of args calling Redis command from script");
329 else
330 *err = sdsnew("Unknown Redis command called from script");
331 return C_ERR;
332 }
333 return C_OK;
334}
335
336static int scriptVerifyACL(client *c, sds *err) {
337 /* Check the ACLs. */
338 int acl_errpos;
339 int acl_retval = ACLCheckAllPerm(c, &acl_errpos);
340 if (acl_retval != ACL_OK) {
341 addACLLogEntry(c,acl_retval,ACL_LOG_CTX_LUA,acl_errpos,NULL,NULL);
342 *err = sdscatfmt(sdsempty(), "The user executing the script %s", getAclErrorMessage(acl_retval));
343 return C_ERR;
344 }
345 return C_OK;
346}
347
348static int scriptVerifyWriteCommandAllow(scriptRunCtx *run_ctx, char **err) {
349
350 /* A write command, on an RO command or an RO script is rejected ASAP.
351 * Note: For scripts, we consider may-replicate commands as write commands.
352 * This also makes it possible to allow read-only scripts to be run during
353 * CLIENT PAUSE WRITE. */
354 if (run_ctx->flags & SCRIPT_READ_ONLY &&
355 (run_ctx->c->cmd->flags & (CMD_WRITE|CMD_MAY_REPLICATE)))
356 {
357 *err = sdsnew("Write commands are not allowed from read-only scripts.");
358 return C_ERR;
359 }
360
361 /* The other checks below are on the server state and are only relevant for
362 * write commands, return if this is not a write command. */
363 if (!(run_ctx->c->cmd->flags & CMD_WRITE))
364 return C_OK;
365
366 /* If the script already made a modification to the dataset, we can't
367 * fail it on unpredictable error state. */
368 if ((run_ctx->flags & SCRIPT_WRITE_DIRTY))
369 return C_OK;
370
371 /* Write commands are forbidden against read-only slaves, or if a
372 * command marked as non-deterministic was already called in the context
373 * of this script. */
374 int deny_write_type = writeCommandsDeniedByDiskError();
375
376 if (server.masterhost && server.repl_slave_ro &&
377 !mustObeyClient(run_ctx->original_client))
378 {
379 *err = sdsdup(shared.roslaveerr->ptr);
380 return C_ERR;
381 }
382
383 if (deny_write_type != DISK_ERROR_TYPE_NONE) {
384 *err = writeCommandsGetDiskErrorMessage(deny_write_type);
385 return C_ERR;
386 }
387
388 /* Don't accept write commands if there are not enough good slaves and
389 * user configured the min-slaves-to-write option. Note this only reachable
390 * for Eval scripts that didn't declare flags, see the other check in
391 * scriptPrepareForRun */
392 if (!checkGoodReplicasStatus()) {
393 *err = sdsdup(shared.noreplicaserr->ptr);
394 return C_ERR;
395 }
396
397 return C_OK;
398}
399
400static int scriptVerifyOOM(scriptRunCtx *run_ctx, char **err) {
401 if (run_ctx->flags & SCRIPT_ALLOW_OOM) {
402 /* Allow running any command even if OOM reached */
403 return C_OK;
404 }
405
406 /* If we reached the memory limit configured via maxmemory, commands that
407 * could enlarge the memory usage are not allowed, but only if this is the
408 * first write in the context of this script, otherwise we can't stop
409 * in the middle. */
410
411 if (server.maxmemory && /* Maxmemory is actually enabled. */
412 !mustObeyClient(run_ctx->original_client) && /* Don't care about mem for replicas or AOF. */
413 !(run_ctx->flags & SCRIPT_WRITE_DIRTY) && /* Script had no side effects so far. */
414 server.pre_command_oom_state && /* Detected OOM when script start. */
415 (run_ctx->c->cmd->flags & CMD_DENYOOM))
416 {
417 *err = sdsdup(shared.oomerr->ptr);
418 return C_ERR;
419 }
420
421 return C_OK;
422}
423
424static int scriptVerifyClusterState(scriptRunCtx *run_ctx, client *c, client *original_c, sds *err) {
425 if (!server.cluster_enabled || mustObeyClient(original_c)) {
426 return C_OK;
427 }
428 /* If this is a Redis Cluster node, we need to make sure the script is not
429 * trying to access non-local keys, with the exception of commands
430 * received from our master or when loading the AOF back in memory. */
431 int error_code;
432 /* Duplicate relevant flags in the script client. */
433 c->flags &= ~(CLIENT_READONLY | CLIENT_ASKING);
434 c->flags |= original_c->flags & (CLIENT_READONLY | CLIENT_ASKING);
435 int hashslot = -1;
436 if (getNodeByQuery(c, c->cmd, c->argv, c->argc, &hashslot, &error_code) != server.cluster->myself) {
437 if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) {
438 *err = sdsnew(
439 "Script attempted to execute a write command while the "
440 "cluster is down and readonly");
441 } else if (error_code == CLUSTER_REDIR_DOWN_STATE) {
442 *err = sdsnew("Script attempted to execute a command while the "
443 "cluster is down");
444 } else {
445 *err = sdsnew("Script attempted to access a non local key in a "
446 "cluster node");
447 }
448 return C_ERR;
449 }
450
451 /* If the script declared keys in advanced, the cross slot error would have
452 * already been thrown. This is only checking for cross slot keys being accessed
453 * that weren't pre-declared. */
454 if (hashslot != -1 && !(run_ctx->flags & SCRIPT_ALLOW_CROSS_SLOT)) {
455 if (original_c->slot == -1) {
456 original_c->slot = hashslot;
457 } else if (original_c->slot != hashslot) {
458 *err = sdsnew("Script attempted to access keys that do not hash to "
459 "the same slot");
460 return C_ERR;
461 }
462 }
463 return C_OK;
464}
465
466/* set RESP for a given run_ctx */
467int scriptSetResp(scriptRunCtx *run_ctx, int resp) {
468 if (resp != 2 && resp != 3) {
469 return C_ERR;
470 }
471
472 run_ctx->c->resp = resp;
473 return C_OK;
474}
475
476/* set Repl for a given run_ctx
477 * either: PROPAGATE_AOF | PROPAGATE_REPL*/
478int scriptSetRepl(scriptRunCtx *run_ctx, int repl) {
479 if ((repl & ~(PROPAGATE_AOF | PROPAGATE_REPL)) != 0) {
480 return C_ERR;
481 }
482 run_ctx->repl_flags = repl;
483 return C_OK;
484}
485
486static int scriptVerifyAllowStale(client *c, sds *err) {
487 if (!server.masterhost) {
488 /* Not a replica, stale is irrelevant */
489 return C_OK;
490 }
491
492 if (server.repl_state == REPL_STATE_CONNECTED) {
493 /* Connected to replica, stale is irrelevant */
494 return C_OK;
495 }
496
497 if (server.repl_serve_stale_data == 1) {
498 /* Disconnected from replica but allow to serve data */
499 return C_OK;
500 }
501
502 if (c->cmd->flags & CMD_STALE) {
503 /* Command is allow while stale */
504 return C_OK;
505 }
506
507 /* On stale replica, can not run the command */
508 *err = sdsnew("Can not execute the command on a stale replica");
509 return C_ERR;
510}
511
512/* Call a Redis command.
513 * The reply is written to the run_ctx client and it is
514 * up to the engine to take and parse.
515 * The err out variable is set only if error occurs and describe the error.
516 * If err is set on reply is written to the run_ctx client. */
517void scriptCall(scriptRunCtx *run_ctx, robj* *argv, int argc, sds *err) {
518 client *c = run_ctx->c;
519
520 /* Setup our fake client for command execution */
521 c->argv = argv;
522 c->argc = argc;
523 c->user = run_ctx->original_client->user;
524
525 /* Process module hooks */
526 moduleCallCommandFilters(c);
527 argv = c->argv;
528 argc = c->argc;
529
530 struct redisCommand *cmd = lookupCommand(argv, argc);
531 c->cmd = c->lastcmd = c->realcmd = cmd;
532 if (scriptVerifyCommandArity(cmd, argc, err) != C_OK) {
533 goto error;
534 }
535
536 /* There are commands that are not allowed inside scripts. */
537 if (!server.script_disable_deny_script && (cmd->flags & CMD_NOSCRIPT)) {
538 *err = sdsnew("This Redis command is not allowed from script");
539 goto error;
540 }
541
542 if (scriptVerifyAllowStale(c, err) != C_OK) {
543 goto error;
544 }
545
546 if (scriptVerifyACL(c, err) != C_OK) {
547 goto error;
548 }
549
550 if (scriptVerifyWriteCommandAllow(run_ctx, err) != C_OK) {
551 goto error;
552 }
553
554 if (scriptVerifyOOM(run_ctx, err) != C_OK) {
555 goto error;
556 }
557
558 if (cmd->flags & CMD_WRITE) {
559 /* signify that we already change the data in this execution */
560 run_ctx->flags |= SCRIPT_WRITE_DIRTY;
561 }
562
563 if (scriptVerifyClusterState(run_ctx, c, run_ctx->original_client, err) != C_OK) {
564 goto error;
565 }
566
567 int call_flags = CMD_CALL_SLOWLOG | CMD_CALL_STATS;
568 if (run_ctx->repl_flags & PROPAGATE_AOF) {
569 call_flags |= CMD_CALL_PROPAGATE_AOF;
570 }
571 if (run_ctx->repl_flags & PROPAGATE_REPL) {
572 call_flags |= CMD_CALL_PROPAGATE_REPL;
573 }
574 call(c, call_flags);
575 serverAssert((c->flags & CLIENT_BLOCKED) == 0);
576 return;
577
578error:
579 afterErrorReply(c, *err, sdslen(*err), 0);
580 incrCommandStatsOnError(cmd, ERROR_COMMAND_REJECTED);
581}
582
583/* Returns the time when the script invocation started */
584mstime_t scriptTimeSnapshot() {
585 serverAssert(curr_run_ctx);
586 return curr_run_ctx->snapshot_time;
587}
588
589long long scriptRunDuration() {
590 serverAssert(scriptIsRunning());
591 return elapsedMs(curr_run_ctx->start_time);
592}
593