root / trunk / twisted / protocols / memcache.py

Revision 26482, 19.4 kB (checked in by exarkun, 4 months ago)

Merge memcache-get-docs-3688

Author: exarkun
Reviewer: thijs
Fixes: #3688

Add API documentation for the return value of the memcache client's get method
in the case where the requested key is not present on the server.

Line 
1 # -*- test-case-name: twisted.test.test_memcache -*-
2 # Copyright (c) 2007-2009 Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5 """
6 Memcache client protocol. Memcached is a caching server, storing data in the
7 form of pairs key/value, and memcache is the protocol to talk with it.
8
9 To connect to a server, create a factory for L{MemCacheProtocol}::
10
11     from twisted.internet import reactor, protocol
12     from twisted.protocols.memcache import MemCacheProtocol, DEFAULT_PORT
13     d = protocol.ClientCreator(reactor, MemCacheProtocol
14         ).connectTCP("localhost", DEFAULT_PORT)
15     def doSomething(proto):
16         # Here you call the memcache operations
17         return proto.set("mykey", "a lot of data")
18     d.addCallback(doSomething)
19     reactor.run()
20
21 All the operations of the memcache protocol are present, but
22 L{MemCacheProtocol.set} and L{MemCacheProtocol.get} are the more important.
23
24 See U{http://code.sixapart.com/svn/memcached/trunk/server/doc/protocol.txt} for
25 more information about the protocol.
26 """
27
28 try:
29     from collections import deque
30 except ImportError:
31     class deque(list):
32         def popleft(self):
33             return self.pop(0)
34
35
36 from twisted.protocols.basic import LineReceiver
37 from twisted.protocols.policies import TimeoutMixin
38 from twisted.internet.defer import Deferred, fail, TimeoutError
39 from twisted.python import log
40
41
42
43 DEFAULT_PORT = 11211
44
45
46
47 class NoSuchCommand(Exception):
48     """
49     Exception raised when a non existent command is called.
50     """
51
52
53
54 class ClientError(Exception):
55     """
56     Error caused by an invalid client call.
57     """
58
59
60
61 class ServerError(Exception):
62     """
63     Problem happening on the server.
64     """
65
66
67
68 class Command(object):
69     """
70     Wrap a client action into an object, that holds the values used in the
71     protocol.
72
73     @ivar _deferred: the L{Deferred} object that will be fired when the result
74         arrives.
75     @type _deferred: L{Deferred}
76
77     @ivar command: name of the command sent to the server.
78     @type command: C{str}
79     """
80
81     def __init__(self, command, **kwargs):
82         """
83         Create a command.
84
85         @param command: the name of the command.
86         @type command: C{str}
87
88         @param kwargs: this values will be stored as attributes of the object
89             for future use
90         """
91         self.command = command
92         self._deferred = Deferred()
93         for k, v in kwargs.items():
94             setattr(self, k, v)
95
96
97     def success(self, value):
98         """
99         Shortcut method to fire the underlying deferred.
100         """
101         self._deferred.callback(value)
102
103
104     def fail(self, error):
105         """
106         Make the underlying deferred fails.
107         """
108         self._deferred.errback(error)
109
110
111
112 class MemCacheProtocol(LineReceiver, TimeoutMixin):
113     """
114     MemCache protocol: connect to a memcached server to store/retrieve values.
115
116     @ivar persistentTimeOut: the timeout period used to wait for a response.
117     @type persistentTimeOut: C{int}
118
119     @ivar _current: current list of requests waiting for an answer from the
120         server.
121     @type _current: C{deque} of L{Command}
122
123     @ivar _lenExpected: amount of data expected in raw mode, when reading for
124         a value.
125     @type _lenExpected: C{int}
126
127     @ivar _getBuffer: current buffer of data, used to store temporary data
128         when reading in raw mode.
129     @type _getBuffer: C{list}
130
131     @ivar _bufferLength: the total amount of bytes in C{_getBuffer}.
132     @type _bufferLength: C{int}
133     """
134     MAX_KEY_LENGTH = 250
135
136     def __init__(self, timeOut=60):
137         """
138         Create the protocol.
139
140         @param timeOut: the timeout to wait before detecting that the
141             connection is dead and close it. It's expressed in seconds.
142         @type timeOut: C{int}
143         """
144         self._current = deque()
145         self._lenExpected = None
146         self._getBuffer = None
147         self._bufferLength = None
148         self.persistentTimeOut = self.timeOut = timeOut
149
150
151     def timeoutConnection(self):
152         """
153         Close the connection in case of timeout.
154         """
155         for cmd in self._current:
156             cmd.fail(TimeoutError("Connection timeout"))
157         self.transport.loseConnection()
158
159
160     def sendLine(self, line):
161         """
162         Override sendLine to add a timeout to response.
163         """
164         if not self._current:
165            self.setTimeout(self.persistentTimeOut)
166         LineReceiver.sendLine(self, line)
167
168
169     def rawDataReceived(self, data):
170         """
171         Collect data for a get.
172         """
173         self.resetTimeout()
174         self._getBuffer.append(data)
175         self._bufferLength += len(data)
176         if self._bufferLength >= self._lenExpected + 2:
177             data = "".join(self._getBuffer)
178             buf = data[:self._lenExpected]
179             rem = data[self._lenExpected + 2:]
180             val = buf
181             self._lenExpected = None
182             self._getBuffer = None
183             self._bufferLength = None
184             cmd = self._current[0]
185             cmd.value = val
186             self.setLineMode(rem)
187
188
189     def cmd_STORED(self):
190         """
191         Manage a success response to a set operation.
192         """
193         self._current.popleft().success(True)
194
195
196     def cmd_NOT_STORED(self):
197         """
198         Manage a specific 'not stored' response to a set operation: this is not
199         an error, but some condition wasn't met.
200         """
201         self._current.popleft().success(False)
202
203
204     def cmd_END(self):
205         """
206         This the end token to a get or a stat operation.
207         """
208         cmd = self._current.popleft()
209         if cmd.command == "get":
210             cmd.success((cmd.flags, cmd.value))
211         elif cmd.command == "gets":
212             cmd.success((cmd.flags, cmd.cas, cmd.value))
213         elif cmd.command == "stats":
214             cmd.success(cmd.values)
215
216
217     def cmd_NOT_FOUND(self):
218         """
219         Manage error response for incr/decr/delete.
220         """
221         self._current.popleft().success(False)
222
223
224     def cmd_VALUE(self, line):
225         """
226         Prepare the reading a value after a get.
227         """
228         cmd = self._current[0]
229         if cmd.command == "get":
230             key, flags, length = line.split()
231             cas = ""
232         else:
233             key, flags, length, cas = line.split()
234         self._lenExpected = int(length)
235         self._getBuffer = []
236         self._bufferLength = 0
237         if cmd.key != key:
238             raise RuntimeError("Unexpected commands answer.")
239         cmd.flags = int(flags)
240         cmd.length = self._lenExpected
241         cmd.cas = cas
242         self.setRawMode()
243
244
245     def cmd_STAT(self, line):
246         """
247         Reception of one stat line.
248         """
249         cmd = self._current[0]
250         key, val = line.split(" ", 1)
251         cmd.values[key] = val
252
253
254     def cmd_VERSION(self, versionData):
255         """
256         Read version token.
257         """
258         self._current.popleft().success(versionData)
259
260
261     def cmd_ERROR(self):
262         """
263         An non-existent command has been sent.
264         """
265         log.err("Non-existent command sent.")
266         cmd = self._current.popleft()
267         cmd.fail(NoSuchCommand())
268
269
270     def cmd_CLIENT_ERROR(self, errText):
271         """
272         An invalid input as been sent.
273         """
274         log.err("Invalid input: %s" % (errText,))
275         cmd = self._current.popleft()
276         cmd.fail(ClientError(errText))
277
278
279     def cmd_SERVER_ERROR(self, errText):
280         """
281         An error has happened server-side.
282         """
283         log.err("Server error: %s" % (errText,))
284         cmd = self._current.popleft()
285         cmd.fail(ServerError(errText))
286
287
288     def cmd_DELETED(self):
289         """
290         A delete command has completed successfully.
291         """
292         self._current.popleft().success(True)
293
294
295     def cmd_OK(self):
296         """
297         The last command has been completed.
298         """
299         self._current.popleft().success(True)
300
301
302     def cmd_EXISTS(self):
303         """
304         A C{checkAndSet} update has failed.
305         """
306         self._current.popleft().success(False)
307
308
309     def lineReceived(self, line):
310         """
311         Receive line commands from the server.
312         """
313         self.resetTimeout()
314         token = line.split(" ", 1)[0]
315         # First manage standard commands without space
316         cmd = getattr(self, "cmd_%s" % (token,), None)
317         if cmd is not None:
318             args = line.split(" ", 1)[1:]
319             if args:
320                 cmd(args[0])
321             else:
322                 cmd()
323         else:
324             # Then manage commands with space in it
325             line = line.replace(" ", "_")
326             cmd = getattr(self, "cmd_%s" % (line,), None)
327             if cmd is not None:
328                 cmd()
329             else:
330                 # Increment/Decrement response
331                 cmd = self._current.popleft()
332                 val = int(line)
333                 cmd.success(val)
334         if not self._current:
335             # No pending request, remove timeout
336             self.setTimeout(None)
337
338
339     def increment(self, key, val=1):
340         """
341         Increment the value of C{key} by given value (default to 1).
342         C{key} must be consistent with an int. Return the new value.
343
344         @param key: the key to modify.
345         @type key: C{str}
346
347         @param val: the value to increment.
348         @type val: C{int}
349
350         @return: a deferred with will be called back with the new value
351             associated with the key (after the increment).
352         @rtype: L{Deferred}
353         """
354         return self._incrdecr("incr", key, val)
355
356
357     def decrement(self, key, val=1):
358         """
359         Decrement the value of C{key} by given value (default to 1).
360         C{key} must be consistent with an int. Return the new value, coerced to
361         0 if negative.
362
363         @param key: the key to modify.
364         @type key: C{str}
365
366         @param val: the value to decrement.
367         @type val: C{int}
368
369         @return: a deferred with will be called back with the new value
370             associated with the key (after the decrement).
371         @rtype: L{Deferred}
372         """
373         return self._incrdecr("decr", key, val)
374
375
376     def _incrdecr(self, cmd, key, val):
377         """
378         Internal wrapper for incr/decr.
379         """
380         if not isinstance(key, str):
381             return fail(ClientError(
382                 "Invalid type for key: %s, expecting a string" % (type(key),)))
383         if len(key) > self.MAX_KEY_LENGTH:
384             return fail(ClientError("Key too long"))
385         fullcmd = "%s %s %d" % (cmd, key, int(val))
386         self.sendLine(fullcmd)
387         cmdObj = Command(cmd, key=key)
388         self._current.append(cmdObj)
389         return cmdObj._deferred
390
391
392     def replace(self, key, val, flags=0, expireTime=0):
393         """
394         Replace the given C{key}. It must already exist in the server.
395
396         @param key: the key to replace.
397         @type key: C{str}
398
399         @param val: the new value associated with the key.
400         @type val: C{str}
401
402         @param flags: the flags to store with the key.
403         @type flags: C{int}
404
405         @param expireTime: if different from 0, the relative time in seconds
406             when the key will be deleted from the store.
407         @type expireTime: C{int}
408
409         @return: a deferred that will fire with C{True} if the operation has
410             succeeded, and C{False} with the key didn't previously exist.
411         @rtype: L{Deferred}
412         """
413         return self._set("replace", key, val, flags, expireTime, "")
414
415
416     def add(self, key, val, flags=0, expireTime=0):
417         """
418         Add the given C{key}. It must not exist in the server.
419
420         @param key: the key to add.
421         @type key: C{str}
422
423         @param val: the value associated with the key.
424         @type val: C{str}
425
426         @param flags: the flags to store with the key.
427         @type flags: C{int}
428
429         @param expireTime: if different from 0, the relative time in seconds
430             when the key will be deleted from the store.
431         @type expireTime: C{int}
432
433         @return: a deferred that will fire with C{True} if the operation has
434             succeeded, and C{False} with the key already exists.
435         @rtype: L{Deferred}
436         """
437         return self._set("add", key, val, flags, expireTime, "")
438
439
440     def set(self, key, val, flags=0, expireTime=0):
441         """
442         Set the given C{key}.
443
444         @param key: the key to set.
445         @type key: C{str}
446
447         @param val: the value associated with the key.
448         @type val: C{str}
449
450         @param flags: the flags to store with the key.
451         @type flags: C{int}
452
453         @param expireTime: if different from 0, the relative time in seconds
454             when the key will be deleted from the store.
455         @type expireTime: C{int}
456
457         @return: a deferred that will fire with C{True} if the operation has
458             succeeded.
459         @rtype: L{Deferred}
460         """
461         return self._set("set", key, val, flags, expireTime, "")
462
463
464     def checkAndSet(self, key, val, cas, flags=0, expireTime=0):
465         """
466         Change the content of C{key} only if the C{cas} value matches the
467         current one associated with the key. Use this to store a value which
468         hasn't been modified since last time you fetched it.
469
470         @param key: The key to set.
471         @type key: C{str}
472
473         @param val: The value associated with the key.
474         @type val: C{str}
475
476         @param cas: Unique 64-bit value returned by previous call of C{get}.
477         @type cas: C{str}
478
479         @param flags: The flags to store with the key.
480         @type flags: C{int}
481
482         @param expireTime: If different from 0, the relative time in seconds
483             when the key will be deleted from the store.
484         @type expireTime: C{int}
485
486         @return: A deferred that will fire with C{True} if the operation has
487             succeeded, C{False} otherwise.
488         @rtype: L{Deferred}
489         """
490         return self._set("cas", key, val, flags, expireTime, cas)
491
492
493     def _set(self, cmd, key, val, flags, expireTime, cas):
494         """
495         Internal wrapper for setting values.
496         """
497         if not isinstance(key, str):
498             return fail(ClientError(
499                 "Invalid type for key: %s, expecting a string" % (type(key),)))
500         if len(key) > self.MAX_KEY_LENGTH:
501             return fail(ClientError("Key too long"))
502         if not isinstance(val, str):
503             return fail(ClientError(
504                 "Invalid type for value: %s, expecting a string" %
505                 (type(val),)))
506         if cas:
507             cas = " " + cas
508         length = len(val)
509         fullcmd = "%s %s %d %d %d%s" % (
510             cmd, key, flags, expireTime, length, cas)
511         self.sendLine(fullcmd)
512         self.sendLine(val)
513         cmdObj = Command(cmd, key=key, flags=flags, length=length)
514         self._current.append(cmdObj)
515         return cmdObj._deferred
516
517
518     def append(self, key, val):
519         """
520         Append given data to the value of an existing key.
521
522         @param key: The key to modify.
523         @type key: C{str}
524
525         @param val: The value to append to the current value associated with
526             the key.
527         @type val: C{str}
528
529         @return: A deferred that will fire with C{True} if the operation has
530             succeeded, C{False} otherwise.
531         @rtype: L{Deferred}
532         """
533         # Even if flags and expTime values are ignored, we have to pass them
534         return self._set("append", key, val, 0, 0, "")
535
536
537     def prepend(self, key, val):
538         """
539         Prepend given data to the value of an existing key.
540
541         @param key: The key to modify.
542         @type key: C{str}
543
544         @param val: The value to prepend to the current value associated with
545             the key.
546         @type val: C{str}
547
548         @return: A deferred that will fire with C{True} if the operation has
549             succeeded, C{False} otherwise.
550         @rtype: L{Deferred}
551         """
552         # Even if flags and expTime values are ignored, we have to pass them
553         return self._set("prepend", key, val, 0, 0, "")
554
555
556     def get(self, key, withIdentifier=False):
557         """
558         Get the given C{key}. It doesn't support multiple keys. If
559         C{withIdentifier} is set to C{True}, the command issued is a C{gets},
560         that will return the current identifier associated with the value. This
561         identifier has to be used when issuing C{checkAndSet} update later,
562         using the corresponding method.
563
564         @param key: The key to retrieve.
565         @type key: C{str}
566
567         @param withIdentifier: If set to C{True}, retrieve the current
568             identifier along with the value and the flags.
569         @type withIdentifier: C{bool}
570
571         @return: A deferred that will fire with the tuple (flags, value) if
572             C{withIdentifier} is C{False}, or (flags, cas identifier, value)
573             if C{True}.  If the server indicates there is no value
574             associated with C{key}, the returned value will be C{None} and
575             the returned flags will be C{0}.
576         @rtype: L{Deferred}
577         """
578         if not isinstance(key, str):
579             return fail(ClientError(
580                 "Invalid type for key: %s, expecting a string" % (type(key),)))
581         if len(key) > self.MAX_KEY_LENGTH:
582             return fail(ClientError("Key too long"))
583         if withIdentifier:
584             cmd = "gets"
585         else:
586             cmd = "get"
587         fullcmd = "%s %s" % (cmd, key)
588         self.sendLine(fullcmd)
589         cmdObj = Command(cmd, key=key, value=None, flags=0, cas="")
590         self._current.append(cmdObj)
591         return cmdObj._deferred
592
593
594     def stats(self, arg=None):
595         """
596         Get some stats from the server. It will be available as a dict.
597
598         @param arg: An optional additional string which will be sent along
599             with the I{stats} command.  The interpretation of this value by
600             the server is left undefined by the memcache protocol
601             specification.
602         @type arg: L{NoneType} or L{str}
603
604         @return: a deferred that will fire with a C{dict} of the available
605             statistics.
606         @rtype: L{Deferred}
607         """
608         cmd = "stats"
609         if arg:
610             cmd = "stats " + arg
611         self.sendLine(cmd)
612         cmdObj = Command("stats", values={})
613         self._current.append(cmdObj)
614         return cmdObj._deferred
615
616
617     def version(self):
618         """
619         Get the version of the server.
620
621         @return: a deferred that will fire with the string value of the
622             version.
623         @rtype: L{Deferred}
624         """
625         self.sendLine("version")
626         cmdObj = Command("version")
627         self._current.append(cmdObj)
628         return cmdObj._deferred
629
630
631     def delete(self, key):
632         """
633         Delete an existing C{key}.
634
635         @param key: the key to delete.
636         @type key: C{str}
637
638         @return: a deferred that will be called back with C{True} if the key
639             was successfully deleted, or C{False} if not.
640         @rtype: L{Deferred}
641         """
642         if not isinstance(key, str):
643             return fail(ClientError(
644                 "Invalid type for key: %s, expecting a string" % (type(key),)))
645         self.sendLine("delete %s" % key)
646         cmdObj = Command("delete", key=key)
647         self._current.append(cmdObj)
648         return cmdObj._deferred
649
650
651     def flushAll(self):
652         """
653         Flush all cached values.
654
655         @return: a deferred that will be called back with C{True} when the
656             operation has succeeded.
657         @rtype: L{Deferred}
658         """
659         self.sendLine("flush_all")
660         cmdObj = Command("flush_all")
661         self._current.append(cmdObj)
662         return cmdObj._deferred
663
664
665
666 __all__ = ["MemCacheProtocol", "DEFAULT_PORT", "NoSuchCommand", "ClientError",
667            "ServerError"]
668
Note: See TracBrowser for help on using the browser.