Skip to content

Commit 964b268

Browse files
Runtime timeouts (#1610)
* Add run-time configuration for default query timeouts * Timeout for write queries that haven't committed changes * define TIMEOUT_NO_TIMEOUT * Refactor timeout logic * Address PR comments * Do not use timeouts for write queries Co-authored-by: swilly22 <[email protected]> Co-authored-by: Roi Lipman <[email protected]>
1 parent db080d4 commit 964b268

File tree

8 files changed

+141
-57
lines changed

8 files changed

+141
-57
lines changed

docs/configuration.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,29 @@ If enabled, RedisGraph will maintain transposed copies of relationship matrices.
8282
$ redis-server --loadmodule ./redisgraph.so MAINTAIN_TRANSPOSED_MATRICES no
8383
```
8484

85+
---
86+
87+
## TIMEOUT
88+
89+
Timeout is a flag that specifies the maximum runtime for read queries in milliseconds. This configuration will not be respected by write queries, to avoid leaving the graph in an inconsistent state.
90+
91+
### Default
92+
93+
`TIMEOUT` is off by default (config value of `0`).
94+
95+
### Example
96+
97+
```
98+
$ redis-server --loadmodule ./redisgraph.so TIMEOUT 1000
99+
```
100+
85101
# Query Configurations
86102

87103
Some configurations may be set per query in the form of additional arguments after the query string. All per-query configurations are off by default unless using a language-specific client, which may establish its own defaults.
88104

89105
## Query Timeout
90106

91-
The query flag `timeout` allows the user to specify the maximum runtime allowed for a query in milliseconds. This configuration can only be set for read queries to avoid leaving the graph in an inconsistent state.
92-
93-
`timeout` may still return partial results followed by an error message indicating the timeout.
107+
The query flag `timeout` allows the user to specify a timeout as described in [TIMEOUT](#TIMEOUT) for a single query.
94108

95109
### Example
96110

src/commands/cmd_dispatcher.c

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "RG.h"
88
#include "commands.h"
9+
#include "../config.h"
910
#include "cmd_context.h"
1011
#include "../util/thpool/pools.h"
1112

@@ -22,9 +23,9 @@ static int _read_flags(RedisModuleString **argv, int argc, bool *compact,
2223
ASSERT(timeout);
2324

2425
// set defaults
25-
*timeout = 0; // no timeout
2626
*compact = false; // verbose
2727
*graph_version = GRAPH_VERSION_MISSING;
28+
Config_Option_get(Config_TIMEOUT, timeout);
2829

2930
// GRAPH.QUERY <GRAPH_KEY> <QUERY>
3031
// make sure we've got more than 3 arguments

src/commands/cmd_query.c

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -259,14 +259,8 @@ void Graph_Query(void *args) {
259259

260260
// set the query timeout if one was specified
261261
if(command_ctx->timeout != 0) {
262-
if(!readonly) {
263-
// disallow timeouts on write operations to avoid leaving the graph in an inconsistent state
264-
ErrorCtx_SetError("Query timeouts may only be specified on read-only queries");
265-
ErrorCtx_EmitException();
266-
goto cleanup;
267-
}
268-
269-
Query_SetTimeOut(command_ctx->timeout, exec_ctx->plan);
262+
// disallow timeouts on write operations to avoid leaving the graph in an inconsistent state
263+
if(readonly) Query_SetTimeOut(command_ctx->timeout, exec_ctx->plan);
270264
}
271265

272266
// populate the container struct for invoking _ExecuteQuery.

src/config.c

Lines changed: 71 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,29 @@
1616
// Configuration parameters
1717
//-----------------------------------------------------------------------------
1818

19-
#define CACHE_SIZE "CACHE_SIZE" // Config param, the size of each thread cache size, per graph.
20-
#define ASYNC_DELETE "ASYNC_DELETE" // whether graphs should be deleted asynchronously
21-
#define THREAD_COUNT "THREAD_COUNT" // Config param, number of threads in thread pool
22-
#define RESULTSET_SIZE "RESULTSET_SIZE" // resultset size limit
23-
#define OMP_THREAD_COUNT "OMP_THREAD_COUNT" // Config param, max number of OpenMP threads
24-
#define VKEY_MAX_ENTITY_COUNT "VKEY_MAX_ENTITY_COUNT" // Config param, max number of entities in each virtual key
25-
#define MAINTAIN_TRANSPOSED_MATRICES "MAINTAIN_TRANSPOSED_MATRICES" // Whether the module should maintain transposed relationship matrices
19+
// config param, the timeout for each query in milliseconds
20+
#define TIMEOUT "TIMEOUT"
21+
22+
// config param, the size of each thread cache size, per graph
23+
#define CACHE_SIZE "CACHE_SIZE"
24+
25+
// whether graphs should be deleted asynchronously
26+
#define ASYNC_DELETE "ASYNC_DELETE"
27+
28+
// config param, number of threads in thread pool
29+
#define THREAD_COUNT "THREAD_COUNT"
30+
31+
// resultset size limit
32+
#define RESULTSET_SIZE "RESULTSET_SIZE"
33+
34+
// config param, max number of OpenMP threads
35+
#define OMP_THREAD_COUNT "OMP_THREAD_COUNT"
36+
37+
// config param, max number of entities in each virtual key
38+
#define VKEY_MAX_ENTITY_COUNT "VKEY_MAX_ENTITY_COUNT"
39+
40+
// whether the module should maintain transposed relationship matrices
41+
#define MAINTAIN_TRANSPOSED_MATRICES "MAINTAIN_TRANSPOSED_MATRICES"
2642

2743
//------------------------------------------------------------------------------
2844
// Configuration defaults
@@ -76,6 +92,18 @@ static inline bool _Config_ParseYesNo(RedisModuleString *rm_str, bool *value) {
7692
// Config access functions
7793
//==============================================================================
7894

95+
//------------------------------------------------------------------------------
96+
// timeout
97+
//------------------------------------------------------------------------------
98+
99+
void Config_timeout_set(uint64_t timeout) {
100+
config.timeout = timeout;
101+
}
102+
103+
uint Config_timeout_get(void) {
104+
return config.timeout;
105+
}
106+
79107
//------------------------------------------------------------------------------
80108
// thread count
81109
//------------------------------------------------------------------------------
@@ -168,6 +196,8 @@ bool Config_Contains_field(const char *field_str, Config_Option_Field *field) {
168196

169197
if(!strcasecmp(field_str, THREAD_COUNT)) {
170198
f = Config_THREAD_POOL_SIZE;
199+
} else if(!strcasecmp(field_str, TIMEOUT)) {
200+
f = Config_TIMEOUT;
171201
} else if(!strcasecmp(field_str, OMP_THREAD_COUNT)) {
172202
f = Config_OPENMP_NTHREAD;
173203
} else if(!strcasecmp(field_str, VKEY_MAX_ENTITY_COUNT)) {
@@ -190,6 +220,10 @@ const char *Config_Field_name(Config_Option_Field field) {
190220
const char *name = NULL;
191221
switch (field)
192222
{
223+
case Config_TIMEOUT:
224+
name = TIMEOUT;
225+
break;
226+
193227
case Config_CACHE_SIZE:
194228
name = CACHE_SIZE;
195229
break;
@@ -260,6 +294,9 @@ void _Config_SetToDefaults(RedisModuleCtx *ctx) {
260294

261295
// no limit on result-set size
262296
config.resultset_size = RESULTSET_SIZE_UNLIMITED;
297+
298+
// no query timeout by default
299+
config.timeout = CONFIG_TIMEOUT_NO_TIMEOUT;
263300
}
264301

265302
int Config_Init(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) {
@@ -310,6 +347,18 @@ bool Config_Option_set(Config_Option_Field field, RedisModuleString *val) {
310347

311348
switch (field)
312349
{
350+
//----------------------------------------------------------------------
351+
// timeout
352+
//----------------------------------------------------------------------
353+
354+
case Config_TIMEOUT:
355+
{
356+
long long timeout;
357+
if(!_Config_ParsePositiveInteger(val, &timeout)) return false;
358+
Config_timeout_set(timeout);
359+
}
360+
break;
361+
313362
//----------------------------------------------------------------------
314363
// cache size
315364
//----------------------------------------------------------------------
@@ -421,6 +470,21 @@ bool Config_Option_get(Config_Option_Field field, ...) {
421470

422471
switch (field)
423472
{
473+
//----------------------------------------------------------------------
474+
// timeout
475+
//----------------------------------------------------------------------
476+
477+
case Config_TIMEOUT:
478+
{
479+
va_start(ap, field);
480+
uint64_t *timeout = va_arg(ap, uint64_t*);
481+
va_end(ap);
482+
483+
ASSERT(timeout != NULL);
484+
(*timeout) = Config_timeout_get();
485+
}
486+
break;
487+
424488
//----------------------------------------------------------------------
425489
// cache size
426490
//----------------------------------------------------------------------

src/config.h

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,25 @@
99
#include <stdbool.h>
1010
#include "redismodule.h"
1111

12-
#define RESULTSET_SIZE_UNLIMITED UINT64_MAX
12+
#define RESULTSET_SIZE_UNLIMITED UINT64_MAX
13+
#define CONFIG_TIMEOUT_NO_TIMEOUT 0
1314
#define VKEY_ENTITY_COUNT_UNLIMITED UINT64_MAX
1415

1516
typedef enum {
16-
Config_CACHE_SIZE = 0, // number of entries in cache
17-
Config_ASYNC_DELETE = 1, // delete graph asynchronously
18-
Config_OPENMP_NTHREAD = 2, // max number of OpenMP threads to use
19-
Config_THREAD_POOL_SIZE = 3, // number of threads in thread pool
20-
Config_RESULTSET_MAX_SIZE = 4, // max number of records in result-set
21-
Config_MAINTAIN_TRANSPOSE = 5, // maintain transpose matrices
22-
Config_VKEY_MAX_ENTITY_COUNT = 6, // max number of elements in vkey
23-
Config_END_MARKER = 7
17+
Config_TIMEOUT = 0, // timeout value for queries
18+
Config_CACHE_SIZE = 1, // number of entries in cache
19+
Config_ASYNC_DELETE = 2, // delete graph asynchronously
20+
Config_OPENMP_NTHREAD = 3, // max number of OpenMP threads to use
21+
Config_THREAD_POOL_SIZE = 4, // number of threads in thread pool
22+
Config_RESULTSET_MAX_SIZE = 5, // max number of records in result-set
23+
Config_MAINTAIN_TRANSPOSE = 6, // maintain transpose matrices
24+
Config_VKEY_MAX_ENTITY_COUNT = 7, // max number of elements in vkey
25+
Config_END_MARKER = 8
2426
} Config_Option_Field;
2527

2628
// configuration object
2729
typedef struct {
30+
uint64_t timeout; // The timeout for each query in milliseconds.
2831
bool async_delete; // If true, graph deletion is done asynchronously.
2932
uint64_t cache_size; // The cache size for each thread, per graph.
3033
uint thread_pool_size; // Thread count for thread pool.
@@ -35,8 +38,8 @@ typedef struct {
3538
} RG_Config;
3639

3740
// Run-time configurable fields
38-
#define RUNTIME_CONFIG_COUNT 1
39-
static const Config_Option_Field RUNTIME_CONFIGS[] = { Config_RESULTSET_MAX_SIZE };
41+
#define RUNTIME_CONFIG_COUNT 2
42+
static const Config_Option_Field RUNTIME_CONFIGS[] = { Config_RESULTSET_MAX_SIZE, Config_TIMEOUT };
4043

4144
// Set module-level configurations to defaults or to user arguments where provided.
4245
// returns REDISMODULE_OK on success, emits an error and returns REDISMODULE_ERR on failure.

src/execution_plan/execution_plan.c

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ static ExecutionPlan *_ExecutionPlan_UnionPlans(AST *ast) {
8989

9090
// Introduce distinct only if `ALL` isn't specified.
9191
const cypher_astnode_t *union_clause = AST_GetClause(ast, CYPHER_AST_UNION,
92-
NULL);
92+
NULL);
9393
if(!cypher_ast_union_has_all(union_clause)) {
9494
OpBase *distinct_op = NewDistinctOp(plan);
9595
ExecutionPlan_AddOp(results_op, distinct_op);
@@ -303,16 +303,6 @@ ExecutionPlan *NewExecutionPlan(void) {
303303
return plan;
304304
}
305305

306-
// Sets an AST segment in the execution plan.
307-
inline void ExecutionPlan_SetAST(ExecutionPlan *plan, AST *ast) {
308-
plan->ast_segment = ast;
309-
}
310-
311-
// Gets the AST segment from the execution plan.
312-
inline AST *ExecutionPlan_GetAST(const ExecutionPlan *plan) {
313-
return plan->ast_segment;
314-
}
315-
316306
void ExecutionPlan_PreparePlan(ExecutionPlan *plan) {
317307
// Plan should be prepared only once.
318308
ASSERT(!plan->prepared);

src/execution_plan/execution_plan.h

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ struct ExecutionPlan {
2828
/* Creates a new execution plan from AST */
2929
ExecutionPlan *NewExecutionPlan(void);
3030

31-
// Sets an AST segment in the execution plan.
32-
void ExecutionPlan_SetAST(ExecutionPlan *plan, AST *ast);
33-
34-
// Gets the AST segment from the execution plan.
35-
AST *ExecutionPlan_GetAST(const ExecutionPlan *plan);
36-
3731
/* Prepare an execution plan for execution: optimize, initialize result set schema. */
3832
void ExecutionPlan_PreparePlan(ExecutionPlan *plan);
3933

tests/flow/test_timeout.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
from RLTest import Env
33
from base import FlowTestsBase
44
from redis import ResponseError
5+
from redisgraph import Graph
56

67
redis_con = None
8+
redis_graph = None
79

810
class testQueryTimeout(FlowTestsBase):
911
def __init__(self):
@@ -13,29 +15,51 @@ def __init__(self):
1315

1416
self.env = Env(decodeResponses=True)
1517
global redis_con
18+
global redis_graph
1619
redis_con = self.env.getConnection()
20+
redis_graph = Graph("timeout", redis_con)
1721

18-
def test_read_query_timeout(self):
22+
def test01_read_query_timeout(self):
1923
query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x"
2024
try:
2125
# The query is expected to time out
22-
redis_con.execute_command("GRAPH.QUERY", "g", query, "timeout", 1)
26+
redis_graph.query(query, timeout=1)
2327
assert(False)
2428
except ResponseError as error:
25-
self.env.assertTrue(isinstance(error, ResponseError))
2629
self.env.assertContains("Query timed out", str(error))
2730

2831
try:
2932
# The query is expected to succeed
30-
redis_con.execute_command("GRAPH.QUERY", "g", query, "timeout", 100)
33+
redis_graph.query(query, timeout=100)
3134
except:
3235
assert(False)
3336

34-
def test_write_query_timeout(self):
35-
query = "create ()"
37+
def test02_configured_timeout(self):
38+
# Verify that the module-level timeout is set to the default of 0
39+
response = redis_con.execute_command("GRAPH.CONFIG GET timeout")
40+
self.env.assertEquals(response[1], 0)
41+
# Set a default timeout of 1 millisecond
42+
redis_con.execute_command("GRAPH.CONFIG SET timeout 1")
43+
response = redis_con.execute_command("GRAPH.CONFIG GET timeout")
44+
self.env.assertEquals(response[1], 1)
45+
46+
# Validate that a read query times out
47+
query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x"
3648
try:
37-
redis_con.execute_command("GRAPH.QUERY", "g", query, "timeout", 1)
49+
redis_graph.query(query)
50+
assert(False)
51+
except ResponseError as error:
52+
self.env.assertContains("Query timed out", str(error))
53+
54+
def test03_write_query_ignore_timeout(self):
55+
# Verify that the timeout argument is ignored by write queries
56+
query = "CREATE (a:M) WITH a UNWIND range(1,10000) AS ctr SET a.v = ctr"
57+
try:
58+
# The query should complete successfully
59+
actual_result = redis_graph.query(query, timeout=1)
60+
# The query should have taken longer than the timeout value
61+
self.env.assertGreater(actual_result.run_time_ms, 1)
62+
# The query should have updated properties 10,000 times
63+
self.env.assertEquals(actual_result.properties_set, 10000)
64+
except ResponseError:
3865
assert(False)
39-
except:
40-
# Expecting an error.
41-
pass

0 commit comments

Comments
 (0)