1
1
import threading
2
2
import socket
3
- from typing import Callable
3
+ from typing import List , Any , Callable
4
4
5
5
from redis .background import BackgroundScheduler
6
6
from redis .exceptions import ConnectionError , TimeoutError
@@ -30,23 +30,22 @@ def __init__(self, config: MultiDbConfig):
30
30
self ._failover_strategy .set_databases (self ._databases )
31
31
self ._auto_fallback_interval = config .auto_fallback_interval
32
32
self ._event_dispatcher = config .event_dispatcher
33
- self ._command_executor = DefaultCommandExecutor (
33
+ self ._command_retry = config .command_retry
34
+ self ._command_retry .update_supported_errors ((ConnectionRefusedError ,))
35
+ self .command_executor = DefaultCommandExecutor (
34
36
failure_detectors = self ._failure_detectors ,
35
37
databases = self ._databases ,
36
- command_retry = config . command_retry ,
38
+ command_retry = self . _command_retry ,
37
39
failover_strategy = self ._failover_strategy ,
38
40
event_dispatcher = self ._event_dispatcher ,
39
41
auto_fallback_interval = self ._auto_fallback_interval ,
40
42
)
41
-
42
- for fd in self ._failure_detectors :
43
- fd .set_command_executor (command_executor = self ._command_executor )
44
-
45
- self ._initialized = False
43
+ self .initialized = False
46
44
self ._hc_lock = threading .RLock ()
47
45
self ._bg_scheduler = BackgroundScheduler ()
46
+ self ._config = config
48
47
49
- def _initialize (self ):
48
+ def initialize (self ):
50
49
"""
51
50
Perform initialization of databases to define their initial state.
52
51
"""
@@ -72,7 +71,7 @@ def raise_exception_on_failed_hc(error):
72
71
# Set states according to a weights and circuit state
73
72
if database .circuit .state == CBState .CLOSED and not is_active_db_found :
74
73
database .state = DBState .ACTIVE
75
- self ._command_executor .active_database = database
74
+ self .command_executor .active_database = database
76
75
is_active_db_found = True
77
76
elif database .circuit .state == CBState .CLOSED and is_active_db_found :
78
77
database .state = DBState .PASSIVE
@@ -82,7 +81,7 @@ def raise_exception_on_failed_hc(error):
82
81
if not is_active_db_found :
83
82
raise NoValidDatabaseException ('Initial connection failed - no active database found' )
84
83
85
- self ._initialized = True
84
+ self .initialized = True
86
85
87
86
def get_databases (self ) -> Databases :
88
87
"""
@@ -110,7 +109,7 @@ def set_active_database(self, database: AbstractDatabase) -> None:
110
109
highest_weighted_db , _ = self ._databases .get_top_n (1 )[0 ]
111
110
highest_weighted_db .state = DBState .PASSIVE
112
111
database .state = DBState .ACTIVE
113
- self ._command_executor .active_database = database
112
+ self .command_executor .active_database = database
114
113
return
115
114
116
115
raise NoValidDatabaseException ('Cannot set active database, database is unhealthy' )
@@ -132,7 +131,7 @@ def add_database(self, database: AbstractDatabase):
132
131
def _change_active_database (self , new_database : AbstractDatabase , highest_weight_database : AbstractDatabase ):
133
132
if new_database .weight > highest_weight_database .weight and new_database .circuit .state == CBState .CLOSED :
134
133
new_database .state = DBState .ACTIVE
135
- self ._command_executor .active_database = new_database
134
+ self .command_executor .active_database = new_database
136
135
highest_weight_database .state = DBState .PASSIVE
137
136
138
137
def remove_database (self , database : Database ):
@@ -144,7 +143,7 @@ def remove_database(self, database: Database):
144
143
145
144
if highest_weight <= weight and highest_weighted_db .circuit .state == CBState .CLOSED :
146
145
highest_weighted_db .state = DBState .ACTIVE
147
- self ._command_executor .active_database = highest_weighted_db
146
+ self .command_executor .active_database = highest_weighted_db
148
147
149
148
def update_database_weight (self , database : AbstractDatabase , weight : float ):
150
149
"""
@@ -182,10 +181,25 @@ def execute_command(self, *args, **options):
182
181
"""
183
182
Executes a single command and return its result.
184
183
"""
185
- if not self ._initialized :
186
- self ._initialize ()
184
+ if not self .initialized :
185
+ self .initialize ()
186
+
187
+ return self .command_executor .execute_command (* args , ** options )
188
+
189
+ def pipeline (self ):
190
+ """
191
+ Enters into pipeline mode of the client.
192
+ """
193
+ return Pipeline (self )
187
194
188
- return self ._command_executor .execute_command (* args , ** options )
195
+ def transaction (self , func : Callable [["Pipeline" ], None ], * watches , ** options ):
196
+ """
197
+ Executes callable as transaction.
198
+ """
199
+ if not self .initialized :
200
+ self .initialize ()
201
+
202
+ return self .command_executor .execute_transaction (func , * watches , * options )
189
203
190
204
def _check_db_health (self , database : AbstractDatabase , on_error : Callable [[Exception ], None ] = None ) -> None :
191
205
"""
@@ -207,7 +221,7 @@ def _check_db_health(self, database: AbstractDatabase, on_error: Callable[[Excep
207
221
database .circuit .state = CBState .OPEN
208
222
elif is_healthy and database .circuit .state != CBState .CLOSED :
209
223
database .circuit .state = CBState .CLOSED
210
- except (ConnectionError , TimeoutError , socket .timeout ) as e :
224
+ except (ConnectionError , TimeoutError , socket .timeout , ConnectionRefusedError ) as e :
211
225
if database .circuit .state != CBState .OPEN :
212
226
database .circuit .state = CBState .OPEN
213
227
is_healthy = False
@@ -219,7 +233,9 @@ def _check_db_health(self, database: AbstractDatabase, on_error: Callable[[Excep
219
233
def _check_databases_health (self , on_error : Callable [[Exception ], None ] = None ):
220
234
"""
221
235
Runs health checks as a recurring task.
236
+ Runs health checks against all databases.
222
237
"""
238
+
223
239
for database , _ in self ._databases :
224
240
self ._check_db_health (database , on_error )
225
241
@@ -232,4 +248,66 @@ def _on_circuit_state_change_callback(self, circuit: CircuitBreaker, old_state:
232
248
self ._bg_scheduler .run_once (DEFAULT_GRACE_PERIOD , _half_open_circuit , circuit )
233
249
234
250
def _half_open_circuit (circuit : CircuitBreaker ):
235
- circuit .state = CBState .HALF_OPEN
251
+ circuit .state = CBState .HALF_OPEN
252
+
253
+
254
+ class Pipeline (RedisModuleCommands , CoreCommands ):
255
+ """
256
+ Pipeline implementation for multiple logical Redis databases.
257
+ """
258
+ def __init__ (self , client : MultiDBClient ):
259
+ self ._command_stack = []
260
+ self ._client = client
261
+
262
+ def __enter__ (self ) -> "Pipeline" :
263
+ return self
264
+
265
+ def __exit__ (self , exc_type , exc_value , traceback ):
266
+ self .reset ()
267
+
268
+ def __del__ (self ):
269
+ try :
270
+ self .reset ()
271
+ except Exception :
272
+ pass
273
+
274
+ def __len__ (self ) -> int :
275
+ return len (self ._command_stack )
276
+
277
+ def __bool__ (self ) -> bool :
278
+ """Pipeline instances should always evaluate to True"""
279
+ return True
280
+
281
+ def reset (self ) -> None :
282
+ self ._command_stack = []
283
+
284
+ def close (self ) -> None :
285
+ """Close the pipeline"""
286
+ self .reset ()
287
+
288
+ def pipeline_execute_command (self , * args , ** options ) -> "Pipeline" :
289
+ """
290
+ Stage a command to be executed when execute() is next called
291
+
292
+ Returns the current Pipeline object back so commands can be
293
+ chained together, such as:
294
+
295
+ pipe = pipe.set('foo', 'bar').incr('baz').decr('bang')
296
+
297
+ At some other point, you can then run: pipe.execute(),
298
+ which will execute all commands queued in the pipe.
299
+ """
300
+ self ._command_stack .append ((args , options ))
301
+ return self
302
+
303
+ def execute_command (self , * args , ** kwargs ):
304
+ return self .pipeline_execute_command (* args , ** kwargs )
305
+
306
+ def execute (self ) -> List [Any ]:
307
+ if not self ._client .initialized :
308
+ self ._client .initialize ()
309
+
310
+ try :
311
+ return self ._client .command_executor .execute_pipeline (tuple (self ._command_stack ))
312
+ finally :
313
+ self .reset ()
0 commit comments