2828 ErrCommandIssue = errors .New ("memc: got command error response" )
2929)
3030
31+ // CAS represents a Compare-And-Swap token used for optimistic locking.
32+ // It is returned by Gets and must be provided to CompareAndSwap to atomically update a value.
33+ type CAS uint64
34+
3135// Options contains configuration parameters that may be applied when executing
3236// a verb like Get, Set, etc.
3337type Options struct {
@@ -437,6 +441,88 @@ func Add[T any](c *Client, key string, item T, opts ...Option) error {
437441 })
438442}
439443
444+ // CompareAndSwap will store the item using the given key, but only if the CAS
445+ // token matches the current value's CAS token. This provides atomic
446+ // compare-and-swap functionality for optimistic locking.
447+ //
448+ // If the key does not exist, ErrNotFound is returned.
449+ //
450+ // If the CAS token does not match (meaning the value was modified since it was
451+ // retrieved with Gets), ErrConflict is returned.
452+ //
453+ // Uses Client c to connect to a memcached instance, and automatically handles
454+ // connection pooling and reuse.
455+ //
456+ // One or more Option(s) may be applied to configure things such as the value
457+ // expiration TTL or its associated flags.
458+ func CompareAndSwap [T any ](c * Client , key string , cas CAS , item T , opts ... Option ) error {
459+ if err := check (key ); err != nil {
460+ return err
461+ }
462+
463+ options := & Options {
464+ expiration : c .expiration ,
465+ flags : 0 ,
466+ }
467+
468+ for _ , opt := range opts {
469+ opt (options )
470+ }
471+
472+ return c .do (key , func (conn * iopool.Buffer ) error {
473+ encoding , encerr := encode (item )
474+ if encerr != nil {
475+ return encerr
476+ }
477+
478+ expiration , experr := c .seconds (options .expiration )
479+ if experr != nil {
480+ return experr
481+ }
482+
483+ // write the header components with CAS token
484+ if _ , err := fmt .Fprintf (
485+ conn ,
486+ "cas %s %d %d %d %d\r \n " ,
487+ key , options .flags , expiration , len (encoding ), cas ,
488+ ); err != nil {
489+ return err
490+ }
491+
492+ // write the payload
493+ if _ , err := conn .Write (encoding ); err != nil {
494+ return err
495+ }
496+
497+ // write clrf
498+ if _ , err := io .WriteString (conn , "\r \n " ); err != nil {
499+ return err
500+ }
501+
502+ // flush the buffer
503+ if err := conn .Flush (); err != nil {
504+ return err
505+ }
506+
507+ // read response
508+ line , lerr := conn .ReadSlice ('\n' )
509+ if lerr != nil {
510+ return lerr
511+ }
512+
513+ switch string (line ) {
514+ case "STORED\r \n " :
515+ return nil
516+ case "NOT_FOUND\r \n " :
517+ return ErrNotFound
518+ case "EXISTS\r \n " :
519+ return ErrConflict
520+ default :
521+ return fmt .Errorf ("memc: unexpected response to cas: %q" , string (line ))
522+ }
523+ })
524+ }
525+
440526// Get the value associated with the given key.
441527//
442528// Uses Client c to connect to a memcached instance, and automatically handles
@@ -472,6 +558,51 @@ func Get[T any](c *Client, key string) (T, error) {
472558 return result , err
473559}
474560
561+ // Gets the value associated with the given key, along with its CAS token.
562+ //
563+ // The CAS token can be used with CompareAndSwap to atomically update the value,
564+ // providing optimistic locking. If the value has been modified since it was
565+ // retrieved, CompareAndSwap will return an ErrConflict error.
566+ //
567+ // Uses Client c to connect to a memcached instance, and automatically handles
568+ // connection pooling and reuse.
569+ func Gets [T any ](c * Client , key string ) (T , CAS , error ) {
570+ var result T
571+ var casToken CAS
572+
573+ if err := check (key ); err != nil {
574+ return result , 0 , err
575+ }
576+
577+ err := c .do (key , func (conn * iopool.Buffer ) error {
578+ // write the header components
579+ if _ , err := fmt .Fprintf (conn , "gets %s\r \n " , key ); err != nil {
580+ return err
581+ }
582+
583+ // flush the connection, forcing bytes over the wire
584+ if err := conn .Flush (); err != nil {
585+ return err
586+ }
587+
588+ // read the response payload with CAS token
589+ payload , cas , err := getPayloadWithCAS (conn .Reader )
590+ if err != nil {
591+ return err
592+ }
593+
594+ result , err = decode [T ](payload )
595+ if err != nil {
596+ return err
597+ }
598+
599+ casToken = CAS (cas )
600+ return nil
601+ })
602+
603+ return result , casToken , err
604+ }
605+
475606func getPayload (r * bufio.Reader ) ([]byte , error ) {
476607 b , err := r .ReadSlice ('\n' )
477608 if err != nil {
@@ -483,7 +614,6 @@ func getPayload(r *bufio.Reader) ([]byte, error) {
483614 return nil , ErrCacheMiss
484615 }
485616
486- // TODO: does not handle CAS value for now
487617 expect := "VALUE %s %d %d\r \n "
488618 var (
489619 key string
@@ -515,6 +645,50 @@ func getPayload(r *bufio.Reader) ([]byte, error) {
515645 return payload , err
516646}
517647
648+ func getPayloadWithCAS (r * bufio.Reader ) ([]byte , uint64 , error ) {
649+ b , err := r .ReadSlice ('\n' )
650+ if err != nil {
651+ return nil , 0 , err
652+ }
653+
654+ // key was not found, is a cache miss
655+ if string (b ) == "END\r \n " {
656+ return nil , 0 , ErrCacheMiss
657+ }
658+
659+ // handle CAS value - format is "VALUE key flags bytes cas\r\n"
660+ expect := "VALUE %s %d %d %d\r \n "
661+ var (
662+ key string
663+ flags int
664+ size int
665+ cas uint64
666+ )
667+
668+ // scan the header line, giving us a payload size and CAS token
669+ if _ , err = fmt .Sscanf (string (b ), expect , & key , & flags , & size , & cas ); err != nil {
670+ return nil , 0 , err
671+ }
672+
673+ // read the data into our payload
674+ payload := make ([]byte , size + 2 ) // including \r\n
675+ if _ , err = io .ReadFull (r , payload ); err != nil {
676+ return nil , 0 , err
677+ }
678+ payload = payload [0 :size ] // chop \r\n
679+
680+ // read the trailing line ("END\r\n")
681+ b , err = r .ReadSlice ('\n' )
682+ if err != nil {
683+ return nil , 0 , err
684+ }
685+ if string (b ) != "END\r \n " {
686+ return nil , 0 , unexpected (b )
687+ }
688+
689+ return payload , cas , nil
690+ }
691+
518692// Delete will remove the value associated with key from memcached.
519693//
520694// Uses Client c to connect to a memcached instance, and automatically handles
0 commit comments