-
Notifications
You must be signed in to change notification settings - Fork 0
/
resp-0.2.tm
473 lines (429 loc) · 12.9 KB
/
resp-0.2.tm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
##################
## Module Name -- resp.tm
## Original Author -- Emmanuel Frecon - emmanuel@sics.se
## Description:
##
## This module implements the base functionality to talk to remote REDIS
## servers through the RESP protocol (REdis Serialisation Protocol).
##
##################
package require Tcl 8.6
namespace eval ::resp {
# This namespace contains the default options for all created REDIS
# connections.
namespace eval vars {
variable -eol "\r\n"
variable -port 6379
variable -auth ""
variable version [lindex [split [file rootname [file tail [info script]]] -] end]
}
namespace export {[a-z]*}
namespace ensemble create
}
# ::resp::connect -- Connect to REDIS host
#
# Connect to a redis host and return an identifier for the connection.
# This identifier will be used in the remaining exported commands of the
# library. Apart from the hostname, this procedure can take the following
# options:
# -port Port to connect to, defaults the default REDIS port.
# -auth Password to authenticate with at connection.
#
# Arguments:
# host Name of host to connect to
# args Dash-led options and their values, see above.
#
# Results:
# Return the identifier for the connection, used throughout this library.
# Return errors on connection problems or authentication failure.
#
# Side Effects:
# None.
proc ::resp::connect { host args } {
# Get options, use the global vars namespace as the source for good
# defaults.
getopt args -port port ${vars::-port}
getopt args -auth paswd ${vars::-auth}
# Now open the socket to the remote server.
set sock [socket $host $port]
fconfigure $sock -translation binary
# Create a variable, in this namespace, using the (unique) name of the
# socket as the name, copy information from the CX dictionary into it.
set cx [namespace current]::$sock
upvar \#0 $cx CX
dict set CX -port $port
dict set CX -auth $paswd
dict set CX -host $host
dict set CX sock $sock; # Remember the socket, even though it's obvious
# Authenticate at server now that we have a connection
if { $paswd ne "" } {
set ok [sync $sock AUTH $paswd]
if { $ok ne "OK" } {
unset $cx
return -code error "Authentication failure at server"
}
}
return $sock; # Return the socket, its our identifier!
}
# ::resp::disconnect -- Disconnect from server
#
# Disconnect from a REDIS server and empty all references to the server.
#
# Arguments:
# sock Identifier of the REDIS connection, as returned from connect
#
# Results:
# None.
#
# Side Effects:
# Might generate errors on connection closing.
proc ::resp::disconnect { sock } {
set cx [namespace current]::$sock
if { ![info exists $cx] } {
return -code error "$sock is not a known connection"
}
unset $cx
close $sock; # Let it fail on purpose
}
# ::resp::command -- Send command to server, return its answer
#
# Sends a REDIS command to a remote server, and possibly wait for its answer.
# The command will automatically be converted to uppercase, if necessary.
# The arguments can contain a number of dash-led options and their value,
# separated from the remaining of the arguments by a double-dash. The only
# option at present is -reply, which contains a callback that will be
# triggered with the content of the reply once it has been made available.
#
# Arguments:
# sock Identifier of the REDIS connection, as returned from connect
# cmd Command to send
# args Arguments to send, possibly led by dash-led options
#
# Results:
# The result of the command is returned, maybe as a list if the answer was
# coded in multi-bulk format. When called in asynchronous mode, this
# procedure will callback the trigger with the content of the answer when
# it has been made available.
#
# Side Effects:
# Might generate errors when writing/reading data to/from socket
proc ::resp::command { sock cmd args } {
# Check that this is an existing connection
set cx [namespace current]::$sock
if { ![info exists $cx] } {
return -code error "$sock is not a known connection"
}
isolate args opts
send $sock $cmd {*}$args
if { [getopt opts -reply cb] } {
fileevent $sock readable [list ReadAndCallback sock cb]
} else {
return [reply $sock]
}
}
# ::resp::send -- Send command to server
#
# Sends a REDIS command to a remote server, not waiting for its answer.
# The command will automatically be converted to uppercase, if necessary.
#
# Arguments:
# sock Identifier of the REDIS connection, as returned from connect
# cmd Command to send
# args Arguments to send
#
# Results:
# None.
#
# Side Effects:
# Might generate errors when writing data to socket
proc ::resp::send { sock cmd args } {
# Check that this is an existing connection
set cx [namespace current]::$sock
if { ![info exists $cx] } {
return -code error "$sock is not a known connection"
}
if { [llength $args] == 0 } {
Write $sock $cmd {*}$args
} else {
MWrite $sock $cmd {*}$args
}
}
# ::resp::reply -- Read reply from server
#
# Read the reply from a command that has been sent to the server using the
# procedure send. This implementation is aware of all the packetisation
# types known to REDIS.
#
# Arguments:
# sock Identifier of the REDIS connection, as returned from connect
#
# Results:
# Return the result of the command, which is represented as a list of
# replies when the answer is in multi bulk.
#
# Side Effects:
# Might generate errors when reading data from socket
proc ::resp::reply { sock } {
# Check that this is an existing connection
set cx [namespace current]::$sock
if { ![info exists $cx] } {
return -code error "$sock is not a known connection"
}
set type [read $sock 1]
switch -exact -- $type {
":" -
"+" {
return [ReadLine $sock]
}
"-" {
set err [ReadLine $sock]
return -code error $err
}
"$" {
return [ReadBulk $sock]
}
"*" {
return [ReadMultiBulk $sock]
}
default {
return -code error "$type is an unknown REDIS header type"
}
}
}
# ::resp::getopt -- Get options
#
# From http://wiki.tcl.tk/17342
#
# Arguments:
# _argv "pointer" to incoming arguments
# name Name of option to extract
# _var Pointer to variable to set
# default Default value
#
# Results:
# 1 if the option was found, 0 otherwise
#
# Side Effects:
# None.
proc ::resp::getopt {_argv name {_var ""} {default ""}} {
upvar 1 $_argv argv $_var var
set pos [lsearch -regexp $argv ^$name]
if {$pos>=0} {
set to $pos
if {$_var ne ""} {
set var [lindex $argv [incr to]]
}
set argv [lreplace $argv $pos $to]
return 1
} else {
if {[llength [info level 0]] == 5} {set var $default}
return 0
}
}
# ::resp::defaults -- Init and option parsing based on namespace.
#
# This procedure takes the dashled variables of a given (sub)namespace to
# initialise a dictionary. These variables are considered as being the
# canonical set of options for a command or object and contain good
# defaults, and the procedure will capture these from the arguments.
#
# Arguments:
# cx_ "Pointer" to dictionary to initialise and parse options in.
# ns Namespace (FQ or relative to caller) where to get options from
# args List of dashled options and arguments, must match content of namespace
#
# Results:
# Return the list of options that were taken from the arguments, an error
# when an option that does not exist in the namespace as a variable was
# found in the arguments.
#
# Side Effects:
# None.
proc ::resp::defaults { cx_ ns args } {
upvar $cx_ CX
set parsed [list]
foreach v [uplevel info vars [string trimright $ns :]::-*] {
set opt [lindex [split $v :] end]
if { [getopt args $opt value [set $v]] } {
lappend parsed $opt
}
dict set CX $opt $value
}
return $parsed
}
# ::resp::isolate -- Isolate options from arguments
#
# Isolate dash-led options from the rest of the arguments. This procedure
# prefers the double-dash as a marker between the options and the
# arguments, but it is also able to traverse until the end of the options
# and the beginning of the arguments. Traversal requires that no value of
# an option starts with a dash to work properly.
#
# Arguments:
# args_ Pointer to list of arguments (will be modified!)
# opts_ Pointer to list of options
#
# Results:
# None.
#
# Side Effects:
# Modifies the args and opts lists that are passed as parameters to
# reflect the arguments and the options.
proc ::resp::isolate { args_ opts_ } {
upvar $args_ args $opts_ opts
set idx [lsearch $args "--"]
if { $idx >= 0 } {
set opts [lrange $args 0 [expr {$idx-1}]]
set args [lrange $args [expr {$idx+1}] end]
} else {
set opts [list]
for {set i 0} {$i <[llength $args] } { incr i 2} {
set opt [lindex $args $i]
set val [lindex $args [expr {$i+1}]]
if { [string index $opt 0] eq "-" } {
if { [string index $val 0] eq "-" } {
incr i -1; # Consider next not next-next!
lappend opts $opt
} else {
lappend opts $opt $val
}
} else {
break
}
}
set args [lrange $args $i end]
}
}
# ::resp::ReadAndCallback -- Callback with response
#
# This procedure is bound to the socket when it is ready to have data for
# reading. It will read a complete response from the server and deliver a
# callback with the response.
#
# Arguments:
# sock Socket to read reply from
# cb Command to callback
#
# Results:
# None.
#
# Side Effects:
# None.
proc ::resp::ReadAndCallback { sock cb } {
fileevent $sock readable {}
set response [reply $sock]
if { [catch {{*}$cb $reply} err] } {
return -code error "Could not callback with reply: $err"
}
}
# ::resp::MWrite -- Multi-bulk write
#
# Write a command and its arguments using the multi-bulk protocol. This is
# especially usefull when sending complex bodies of jobs for DISQUE.
#
# Arguments:
# sock Socket to REDIS node.
# cmd Command to send
# args Arguments to command, each will lead to a "bulk"
#
# Results:
# None.
#
# Side Effects:
# None.
proc ::resp::MWrite { sock cmd args } {
set len [llength $args];
incr len; # Count the command as well
puts -nonewline $sock "*$len${vars::-eol}"
set len [string length $cmd]
puts -nonewline $sock "\$$len${vars::-eol}$cmd${vars::-eol}"
foreach arg $args {
set len [string length $arg]
puts -nonewline $sock "\$$len${vars::-eol}$arg${vars::-eol}"
}
flush $sock
}
# ::resp::Write -- Write in regular mode
#
# Send a command and its arguments to the REDIS server in the regular
# "one-line" mode.
#
# Arguments:
# sock Socket to REDIS node
# cmd Command to send
# args Arguments to command.
#
# Results:
# None.
#
# Side Effects:
# None.
proc ::resp::Write { sock cmd args } {
puts -nonewline $sock [string toupper $cmd]
if { [llength $args] } {
puts -nonewline $sock " "
puts -nonewline $sock $args
}
puts -nonewline $sock ${vars::-eol}
flush $sock
}
# ::resp::ReadMultiBulk -- Read several bulks
#
# Read a multi-bulk formatted block of data from the REDIS socket.
#
# Arguments:
# sock Socket to REDIS node
#
# Results:
# Return a list of each bulk that was read.
#
# Side Effects:
# None.
proc ::resp::ReadMultiBulk { sock } {
set len [ReadLine $sock]
if { $len < 0 } {
return
}
set reply [list]
for { set i 0 } { $i < $len } { incr i } {
lappend reply [reply $sock]
}
return $reply
}
# ::resp::ReadBulk -- Read one bulk
#
# Read one bulk from the REDIS socket.
#
# Arguments:
# sock Socket to REDIS node
#
# Results:
# Return the bulk that was read
#
# Side Effects:
# None.
proc ::resp::ReadBulk { sock } {
set len [ReadLine $sock]
if { $len < 0 } {
return
}
set buf [read $sock $len]
read $sock [string length ${vars::-eol}]
return $buf
}
# ::resp::ReadLine -- Read one line
#
# Read a single line from the REDIS socket.
#
# Arguments:
# sock Socket to REDIS node.
#
# Results:
# Read line that was read, leading and trailing spaces are trimmed away.
#
# Side Effects:
# None.
proc ::resp::ReadLine { sock } {
return [string trim [gets $sock]]
}
package provide resp $::resp::vars::version