├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── g_counter.go ├── g_counter_test.go ├── g_set.go ├── g_set_test.go ├── lww_e_set.go ├── lww_e_set_test.go ├── or_set.go ├── or_set_test.go ├── pn_counter.go ├── pn_counter_test.go ├── set.go ├── twophase_set.go └── twophase_set_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.out 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | - 1.5 6 | before_install: 7 | - go get github.com/axw/gocov/gocov 8 | - go get github.com/mattn/goveralls 9 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 10 | script: 11 | - $HOME/gopath/bin/goveralls -service=travis-ci 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Vaibhav Bhembre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CRDT [![Build Status](https://travis-ci.org/neurodrone/crdt.svg?branch=master)](https://travis-ci.org/neurodrone/crdt) [![Coverage Status](https://coveralls.io/repos/neurodrone/crdt/badge.svg?branch=master&service=github)](https://coveralls.io/github/neurodrone/crdt?branch=master) [![GoDoc](https://godoc.org/github.com/neurodrone/crdt?status.svg)](https://godoc.org/github.com/neurodrone/crdt) [![](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/neurodrone/crdt/blob/master/LICENSE) [![Report card](http://goreportcard.com/badge/neurodrone/crdt)](http://goreportcard.com/report/neurodrone/crdt) 2 | 3 | This is an implementation of [Convergent and Commutative Replicated Data Types](https://hal.inria.fr/inria-00555588/document) in [Go](https://golang.org/). 4 | 5 | The following state-based counters and sets have currently been 6 | implemented. 7 | 8 | ## Counters 9 | 10 | ### G-Counter 11 | 12 | A grow-only counter (G-Counter) can only increase in one direction. The increment 13 | operation increases the value of current replica by 1. The merge 14 | operation combines values from distinct replicas by taking the maximum 15 | of each replica. 16 | 17 | ```go 18 | gcounter := crdt.NewGCounter() 19 | 20 | // We can increase the counter monotonically by 1. 21 | gcounter.Inc() 22 | 23 | // Twice. 24 | gcounter.Inc() 25 | 26 | // Or we can pass in an arbitrary delta to apply as an increment. 27 | gcounter.IncVal(2) 28 | 29 | // Should print '4' as the result. 30 | fmt.Println(gcounter.Count()) 31 | ``` 32 | 33 | ### PN-Counter 34 | 35 | A positive-negative counter (PN-Counter) is a CRDT that can both increase or 36 | decrease and converge correctly in the light of commutative 37 | operations. Both `.Inc()` and `.Dec()` operations are allowed and thus 38 | negative values are possible as a result. 39 | 40 | ```go 41 | pncounter := crdt.NewPNCounter() 42 | 43 | // We can increase the counter by 1. 44 | pncounter.Inc() 45 | 46 | // Or more. 47 | pncounter.Inc(100) 48 | 49 | // And similarly decrease its value by 1. 50 | pncounter.Dec() 51 | 52 | // Or more. 53 | pncounter.DecVal(100) 54 | 55 | // End result should equal '0' here. 56 | fmt.Println(pncounter.Count()) 57 | ``` 58 | 59 | ## Sets 60 | 61 | ### G-Set 62 | 63 | A grow-only (G-Set) set to which element/s can be added to. Removing element 64 | from the set is not possible. 65 | 66 | ```go 67 | obj := "dummy-object" 68 | gset := crdt.NewGSet() 69 | 70 | gset.Add(obj) 71 | 72 | // Should always print 'true' as `obj` exists in the g-set. 73 | fmt.Println(gset.Contains(obj)) 74 | ``` 75 | 76 | ### 2P-Set 77 | 78 | Two-phase set (2P-Set) allows both additions and removals to the set. 79 | Internally it comprises of two G-Sets, one to keep track of additions 80 | and the other for removals. 81 | 82 | ```go 83 | obj := "dummy-object" 84 | 85 | ppset := crdt.NewTwoPhaseSet() 86 | 87 | ppset.Add(obj) 88 | 89 | // Remove the object that we just added to the set, emptying it. 90 | ppset.Remove(obj) 91 | 92 | // Should return 'false' as the obj doesn't exist within the set. 93 | fmt.Println(ppset.Contains(obj)) 94 | ``` 95 | 96 | ### LWW-e-Set 97 | 98 | Last-write-wins element set (LWW-e-Set) keeps track of element additions 99 | and removals but with respect to the timestamp that is attached to each 100 | element. Timestamps should be unique and have ordering properties. 101 | 102 | ```go 103 | obj := "dummy-object" 104 | lwwset := crdt.NewLWWSet() 105 | 106 | // Here, we remove the object first before we add it in. For a 107 | // 2P-set the object would be deemed absent from the set. But for 108 | // a LWW-set the object should be present because `.Add()` follows 109 | // `.Remove()`. 110 | lwwset.Remove(obj); lwwset.Add(obj) 111 | 112 | // This should print 'true' because of the above. 113 | fmt.Println(lwwset.Contains(obj)) 114 | ``` 115 | 116 | ### OR-Set 117 | 118 | An OR-Set (Observed-Removed-Set) allows deletion and addition of 119 | elements similar to LWW-e-Set, but does not surface only the most recent one. Additions are uniquely tracked via tags and an element is considered member of the set if the deleted set consists of all the tags present within additions. 120 | 121 | ```go 122 | // object 1 == object 2 123 | obj1, obj2 := "dummy-object", "dummy-object" 124 | 125 | orset := crdt.NewORSet() 126 | 127 | orset.Add(obj1); orset.Add(obj2) 128 | 129 | // Removing any one of the above two objects should remove both 130 | // because they contain the same value. 131 | orset.Remove(obj1) 132 | 133 | // Should return 'false'. 134 | fmt.Println(orset.Contains(obj2)) 135 | ``` 136 | -------------------------------------------------------------------------------- /g_counter.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "github.com/satori/go.uuid" 4 | 5 | // GCounter represent a G-counter in CRDT, which is 6 | // a state-based grow-only counter that only supports 7 | // increments. 8 | type GCounter struct { 9 | // ident provides a unique identity to each replica. 10 | ident string 11 | 12 | // counter maps identity of each replica to their 13 | // entry values i.e. the counter value they individually 14 | // have. 15 | counter map[string]int 16 | } 17 | 18 | // NewGCounter returns a *GCounter by pre-assigning a unique 19 | // identity to it. 20 | func NewGCounter() *GCounter { 21 | return &GCounter{ 22 | ident: uuid.NewV4().String(), 23 | counter: make(map[string]int), 24 | } 25 | } 26 | 27 | // Inc increments the GCounter by the value of 1 everytime it 28 | // is called. 29 | func (g *GCounter) Inc() { 30 | g.IncVal(1) 31 | } 32 | 33 | // IncVal allows passing in an arbitrary delta to increment the 34 | // current value of counter by. Only positive values are accepted. 35 | // If a negative value is provided the implementation will panic. 36 | func (g *GCounter) IncVal(incr int) { 37 | if incr < 0 { 38 | panic("cannot decrement a gcounter") 39 | } 40 | g.counter[g.ident] += incr 41 | } 42 | 43 | // Count returns the total count of this counter across all the 44 | // present replicas. 45 | func (g *GCounter) Count() (total int) { 46 | for _, val := range g.counter { 47 | total += val 48 | } 49 | return 50 | } 51 | 52 | // Merge combines the counter values across multiple replicas. 53 | // The property of idempotency is preserved here across 54 | // multiple merges as when no state is changed across any replicas, 55 | // the result should be exactly the same everytime. 56 | func (g *GCounter) Merge(c *GCounter) { 57 | for ident, val := range c.counter { 58 | if v, ok := g.counter[ident]; !ok || v < val { 59 | g.counter[ident] = val 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /g_counter_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "testing" 4 | 5 | func TestGCounter(t *testing.T) { 6 | for _, tt := range []struct { 7 | incsOne int 8 | incsTwo int 9 | result int 10 | }{ 11 | {5, 10, 15}, 12 | {10, 5, 15}, 13 | {100, 100, 200}, 14 | {1, 2, 3}, 15 | } { 16 | gOne, gTwo := NewGCounter(), NewGCounter() 17 | 18 | for i := 0; i < tt.incsOne; i++ { 19 | gOne.Inc() 20 | } 21 | 22 | for i := 0; i < tt.incsTwo; i++ { 23 | gTwo.Inc() 24 | } 25 | 26 | gOne.Merge(gTwo) 27 | 28 | if gOne.Count() != tt.result { 29 | t.Errorf("expected total count to be: %d, actual: %d", 30 | tt.result, 31 | gOne.Count()) 32 | } 33 | 34 | gTwo.Merge(gOne) 35 | 36 | if gTwo.Count() != tt.result { 37 | t.Errorf("expected total count to be: %d, actual: %d", 38 | tt.result, 39 | gTwo.Count()) 40 | } 41 | } 42 | } 43 | 44 | func TestGCounterInvalidInput(t *testing.T) { 45 | gc := NewGCounter() 46 | 47 | defer func() { 48 | if r := recover(); r == nil { 49 | t.Fatalf("panic expected here") 50 | } 51 | }() 52 | 53 | gc.IncVal(-5) 54 | } 55 | -------------------------------------------------------------------------------- /g_set.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "encoding/json" 4 | 5 | type mainSet map[interface{}]struct{} 6 | 7 | var ( 8 | // GSet should implement the set interface. 9 | _ Set = &GSet{} 10 | ) 11 | 12 | // Gset is a grow-only set. 13 | type GSet struct { 14 | mainSet mainSet 15 | } 16 | 17 | // NewGSet returns an instance of GSet. 18 | func NewGSet() *GSet { 19 | return &GSet{ 20 | mainSet: mainSet{}, 21 | } 22 | } 23 | 24 | // Add lets you add an element to grow-only set. 25 | func (g *GSet) Add(elem interface{}) { 26 | g.mainSet[elem] = struct{}{} 27 | } 28 | 29 | // Contains returns true if an element exists within the 30 | // set or false otherwise. 31 | func (g *GSet) Contains(elem interface{}) bool { 32 | _, ok := g.mainSet[elem] 33 | return ok 34 | } 35 | 36 | // Len returns the no. of elements present within GSet. 37 | func (g *GSet) Len() int { 38 | return len(g.mainSet) 39 | } 40 | 41 | // Elems returns all the elements present in the set. 42 | func (g *GSet) Elems() []interface{} { 43 | elems := make([]interface{}, 0, len(g.mainSet)) 44 | 45 | for elem := range g.mainSet { 46 | elems = append(elems, elem) 47 | } 48 | 49 | return elems 50 | } 51 | 52 | type gsetJSON struct { 53 | T string `json:"type"` 54 | E []interface{} `json:"e"` 55 | } 56 | 57 | // MarshalJSON will be used to generate a serialized output 58 | // of a given GSet. 59 | func (g *GSet) MarshalJSON() ([]byte, error) { 60 | return json.Marshal(&gsetJSON{ 61 | T: "g-set", 62 | E: g.Elems(), 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /g_set_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestGSetAddContains(t *testing.T) { 10 | gset := NewGSet() 11 | 12 | elem := "some-test-element" 13 | if gset.Contains(elem) { 14 | t.Errorf("set should not contain %q element", elem) 15 | } 16 | 17 | gset.Add(elem) 18 | if !gset.Contains(elem) { 19 | t.Errorf("set should contain %q element", elem) 20 | } 21 | 22 | expectedCount := 1 23 | if gset.Len() != expectedCount { 24 | t.Errorf("set expected to contain %d elements, actual: %d", expectedCount, gset.Len()) 25 | } 26 | } 27 | 28 | func TestGSetElems(t *testing.T) { 29 | for _, tt := range []struct { 30 | add []interface{} 31 | }{ 32 | {[]interface{}{}}, 33 | {[]interface{}{1}}, 34 | {[]interface{}{1, 2, 3}}, 35 | {[]interface{}{1, 100, 1000, -1}}, 36 | {[]interface{}{"alpha"}}, 37 | {[]interface{}{"alpha", "beta"}}, 38 | {[]interface{}{"alpha", "beta", 1, 2}}, 39 | } { 40 | gset := NewGSet() 41 | 42 | expectedElems := map[interface{}]struct{}{} 43 | for _, i := range tt.add { 44 | expectedElems[i] = struct{}{} 45 | gset.Add(i) 46 | } 47 | 48 | actualElems := map[interface{}]struct{}{} 49 | for _, i := range gset.Elems() { 50 | actualElems[i] = struct{}{} 51 | } 52 | 53 | if !reflect.DeepEqual(expectedElems, actualElems) { 54 | t.Errorf("expected set to contain: %v, actual: %v", expectedElems, actualElems) 55 | } 56 | } 57 | } 58 | 59 | func TestGSetMarshalJSON(t *testing.T) { 60 | for _, tt := range []struct { 61 | add []interface{} 62 | expected string 63 | }{ 64 | {[]interface{}{}, `{"type":"g-set","e":[]}`}, 65 | {[]interface{}{1}, `{"type":"g-set","e":[1]}`}, 66 | {[]interface{}{1, 2, 3}, `{"type":"g-set","e":[3,2,1]}`}, 67 | {[]interface{}{1, 2, 3}, `{"type":"g-set","e":[1,2,3]}`}, 68 | {[]interface{}{"alpha"}, `{"type":"g-set","e":["alpha"]}`}, 69 | {[]interface{}{"alpha", "beta", "gamma"}, `{"type":"g-set","e":["alpha","beta","gamma"]}`}, 70 | {[]interface{}{"alpha", 1, "beta", 2}, `{"type":"g-set","e":[1,2,"alpha","beta"]}`}, 71 | } { 72 | 73 | gset := NewGSet() 74 | 75 | for _, e := range tt.add { 76 | gset.Add(e) 77 | } 78 | 79 | out, err := json.Marshal(gset) 80 | if err != nil { 81 | t.Fatalf("unexpected error on marshalling gset: %s", err) 82 | } 83 | 84 | a := struct { 85 | E []interface{} `json:"e"` 86 | }{} 87 | 88 | if err = json.Unmarshal(out, &a); err != nil { 89 | t.Fatalf("unexpected error on unmarshalling serialized %q: %s", tt.expected, err) 90 | } 91 | 92 | expectedMap := map[interface{}]struct{}{} 93 | for _, i := range a.E { 94 | expectedMap[i] = struct{}{} 95 | } 96 | 97 | if err = json.Unmarshal([]byte(tt.expected), &a); err != nil { 98 | t.Fatalf("unexpected error on unmarshalling serialized %q: %s", tt.expected, err) 99 | } 100 | 101 | actualMap := map[interface{}]struct{}{} 102 | for _, i := range a.E { 103 | actualMap[i] = struct{}{} 104 | } 105 | 106 | if !reflect.DeepEqual(expectedMap, actualMap) { 107 | t.Errorf("expected set to contain: %v, actual: %v", expectedMap, actualMap) 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /lww_e_set.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "time" 7 | 8 | "github.com/benbjohnson/clock" 9 | ) 10 | 11 | type LWWSet struct { 12 | addMap map[interface{}]time.Time 13 | rmMap map[interface{}]time.Time 14 | 15 | bias BiasType 16 | 17 | clock clock.Clock 18 | } 19 | 20 | type BiasType string 21 | 22 | const ( 23 | BiasAdd BiasType = "a" 24 | BiasRemove BiasType = "r" 25 | ) 26 | 27 | var ( 28 | ErrNoSuchBias = errors.New("no such bias found") 29 | ) 30 | 31 | func NewLWWSet() (*LWWSet, error) { 32 | return NewLWWSetWithBias(BiasAdd) 33 | } 34 | 35 | func NewLWWSetWithBias(bias BiasType) (*LWWSet, error) { 36 | if bias != BiasAdd && bias != BiasRemove { 37 | return nil, ErrNoSuchBias 38 | } 39 | 40 | return &LWWSet{ 41 | addMap: make(map[interface{}]time.Time), 42 | rmMap: make(map[interface{}]time.Time), 43 | bias: bias, 44 | clock: clock.New(), 45 | }, nil 46 | } 47 | 48 | func (s *LWWSet) Add(value interface{}) { 49 | s.addMap[value] = s.clock.Now() 50 | } 51 | 52 | func (s *LWWSet) Remove(value interface{}) { 53 | s.rmMap[value] = s.clock.Now() 54 | } 55 | 56 | func (s *LWWSet) Contains(value interface{}) bool { 57 | addTime, addOk := s.addMap[value] 58 | 59 | // If a value is not present in added set then 60 | // always return false, irrespective of whether 61 | // it is present in the removed set. 62 | if !addOk { 63 | return false 64 | } 65 | 66 | rmTime, rmOk := s.rmMap[value] 67 | 68 | // If a value is present in added set and not in remove 69 | // we should always return true. 70 | if !rmOk { 71 | return true 72 | } 73 | 74 | switch s.bias { 75 | case BiasAdd: 76 | return !addTime.Before(rmTime) 77 | 78 | case BiasRemove: 79 | return rmTime.Before(addTime) 80 | } 81 | 82 | // This case will almost always never be hit. Usually 83 | // if an invalid Bias value is provided, it is called 84 | // at a higher level. 85 | return false 86 | } 87 | 88 | func (s *LWWSet) Merge(r *LWWSet) { 89 | for value, ts := range r.addMap { 90 | if t, ok := s.addMap[value]; ok && t.Before(ts) { 91 | s.addMap[value] = ts 92 | } else { 93 | if t.Before(ts) { 94 | s.addMap[value] = ts 95 | } else { 96 | s.addMap[value] = t 97 | } 98 | } 99 | } 100 | 101 | for value, ts := range r.rmMap { 102 | if t, ok := s.rmMap[value]; ok && t.Before(ts) { 103 | s.rmMap[value] = ts 104 | } else { 105 | if t.Before(ts) { 106 | s.rmMap[value] = ts 107 | } else { 108 | s.rmMap[value] = t 109 | } 110 | } 111 | } 112 | } 113 | 114 | type lwwesetJSON struct { 115 | T string `json:"type"` 116 | B string `json:"bias"` 117 | E []elJSON `json:"e"` 118 | } 119 | 120 | type elJSON struct { 121 | Elem interface{} `json:"el"` 122 | TAdd int64 `json:"ta,omitempty"` 123 | TDel int64 `json:"td,omitempty"` 124 | } 125 | 126 | func (s *LWWSet) MarshalJSON() ([]byte, error) { 127 | l := &lwwesetJSON{ 128 | T: "lww-e-set", 129 | B: string(s.bias), 130 | E: make([]elJSON, 0, len(s.addMap)), 131 | } 132 | 133 | for e, t := range s.addMap { 134 | el := elJSON{Elem: e, TAdd: t.Unix()} 135 | if td, ok := s.rmMap[e]; ok { 136 | el.TDel = td.Unix() 137 | } 138 | 139 | l.E = append(l.E, el) 140 | } 141 | 142 | for e, t := range s.rmMap { 143 | if _, ok := s.addMap[e]; ok { 144 | continue 145 | } 146 | 147 | l.E = append(l.E, elJSON{Elem: e, TDel: t.Unix()}) 148 | } 149 | 150 | return json.Marshal(l) 151 | } 152 | -------------------------------------------------------------------------------- /lww_e_set_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | "time" 8 | 9 | "github.com/benbjohnson/clock" 10 | ) 11 | 12 | func TestLWWESetAddContains(t *testing.T) { 13 | lww, err := NewLWWSet() 14 | if err != nil { 15 | t.Fatalf("Unexpected error creating lwwset: %s", err) 16 | } 17 | 18 | testStr := "object1" 19 | if lww.Contains(testStr) { 20 | t.Errorf("set should not contain elem: %q", testStr) 21 | } 22 | 23 | lww.Add(testStr) 24 | 25 | if !lww.Contains(testStr) { 26 | t.Errorf("Expected set to contain: %v, but not found", testStr) 27 | } 28 | } 29 | 30 | func TestLWWESetAddRemoveContains(t *testing.T) { 31 | lww, err := NewLWWSet() 32 | if err != nil { 33 | t.Fatalf("Unexpected error creating lwwset: %s", err) 34 | } 35 | 36 | testStr := "object2" 37 | lww.Add(testStr) 38 | lww.Remove(testStr) 39 | 40 | if lww.Contains(testStr) { 41 | t.Errorf("Expected set to not contain: %v, but found", testStr) 42 | } 43 | } 44 | 45 | func TestInvalidBias(t *testing.T) { 46 | var InvalidBias BiasType = "invalid" 47 | 48 | if _, err := NewLWWSetWithBias(InvalidBias); err != ErrNoSuchBias { 49 | t.Errorf("error expected here when invalid bias provided: %s", err) 50 | } 51 | 52 | mock := clock.NewMock() 53 | 54 | lww := &LWWSet{ 55 | addMap: make(map[interface{}]time.Time), 56 | rmMap: make(map[interface{}]time.Time), 57 | bias: InvalidBias, 58 | clock: mock, 59 | } 60 | 61 | elem := "object1" 62 | 63 | // Remove the element before it is added. Since the time of adding 64 | // the element is greater than the time to remove it this set should 65 | // technically comprise of that element. But because the Bias is invalid 66 | // verify that it should always generate a false response. 67 | lww.Add(elem) 68 | mock.Add(-1 * time.Minute) 69 | lww.Remove(elem) 70 | 71 | if lww.Contains(elem) { 72 | t.Errorf("set should not contain element and should trigger an invalid case") 73 | } 74 | 75 | } 76 | 77 | func TestLWWESetAddRemoveConflict(t *testing.T) { 78 | for _, tt := range []struct { 79 | bias BiasType 80 | testObject string 81 | elapsed time.Duration 82 | testFn func(*LWWSet, interface{}) bool 83 | }{ 84 | { 85 | BiasAdd, 86 | "object2", 87 | 0, 88 | func(l *LWWSet, obj interface{}) bool { return l.Contains(obj) }, 89 | }, 90 | { 91 | BiasRemove, 92 | "object3", 93 | 0, 94 | func(l *LWWSet, obj interface{}) bool { return !l.Contains(obj) }, 95 | }, 96 | { 97 | BiasAdd, 98 | "object4", 99 | 1 * time.Minute, 100 | func(l *LWWSet, obj interface{}) bool { return !l.Contains(obj) }, 101 | }, 102 | { 103 | BiasAdd, 104 | "object5", 105 | -1 * time.Minute, 106 | func(l *LWWSet, obj interface{}) bool { return l.Contains(obj) }, 107 | }, 108 | { 109 | BiasRemove, 110 | "object6", 111 | 1 * time.Minute, 112 | func(l *LWWSet, obj interface{}) bool { return !l.Contains(obj) }, 113 | }, 114 | { 115 | BiasRemove, 116 | "object7", 117 | -1 * time.Minute, 118 | func(l *LWWSet, obj interface{}) bool { return l.Contains(obj) }, 119 | }, 120 | } { 121 | // Create a LWW Set by explicitly setting a bias. 122 | lww, err := NewLWWSetWithBias(tt.bias) 123 | if err != nil { 124 | t.Fatalf("Unexpected error creating lwwset: %s", err) 125 | } 126 | 127 | // Mock the time so we can time travel forward and back. 128 | mock := clock.NewMock() 129 | lww.clock = mock 130 | 131 | // Create an object that will be removed: 132 | // a. right the very moment it is added 133 | // b. in future 134 | // c. in past 135 | lww.Add(tt.testObject) 136 | 137 | // This will be our time travel tuner for now. 138 | mock.Add(tt.elapsed) 139 | 140 | lww.Remove(tt.testObject) 141 | 142 | // Verify that the object is correctly present or absent from the LWW set. 143 | if !tt.testFn(lww, tt.testObject) { 144 | t.Errorf("value: '%v' in in invalid state in the set when bias: %q", tt.testObject, tt.bias) 145 | } 146 | } 147 | } 148 | 149 | func TestLWWESetMerge(t *testing.T) { 150 | type addRm struct { 151 | op string 152 | d time.Duration 153 | } 154 | 155 | var addOp, rmOp string = "add", "remove" 156 | 157 | for _, tt := range []struct { 158 | mapOne, mapTwo map[string]addRm 159 | valid, invalid map[string]struct{} 160 | }{ 161 | { 162 | map[string]addRm{ 163 | "object1": addRm{addOp, 1 * time.Minute}, 164 | "object2": addRm{addOp, 2 * time.Minute}, 165 | }, 166 | map[string]addRm{ 167 | "object1": addRm{rmOp, 2 * time.Minute}, 168 | "object2": addRm{rmOp, 2 * time.Minute}, 169 | }, 170 | map[string]struct{}{ 171 | "object2": struct{}{}, 172 | }, 173 | map[string]struct{}{ 174 | "object1": struct{}{}, 175 | }, 176 | }, 177 | { 178 | map[string]addRm{ 179 | "object1": addRm{addOp, 1 * time.Minute}, 180 | "object2": addRm{rmOp, 2 * time.Minute}, 181 | }, 182 | map[string]addRm{ 183 | "object3": addRm{addOp, 1 * time.Minute}, 184 | "object4": addRm{rmOp, 2 * time.Minute}, 185 | }, 186 | map[string]struct{}{ 187 | "object1": struct{}{}, 188 | "object3": struct{}{}, 189 | }, 190 | map[string]struct{}{ 191 | "object2": struct{}{}, 192 | "object4": struct{}{}, 193 | }, 194 | }, 195 | { 196 | map[string]addRm{ 197 | "object1": addRm{addOp, 1 * time.Minute}, 198 | "object2": addRm{addOp, 3 * time.Minute}, 199 | }, 200 | map[string]addRm{ 201 | "object1": addRm{addOp, 2 * time.Minute}, 202 | "object2": addRm{addOp, 2 * time.Minute}, 203 | }, 204 | map[string]struct{}{ 205 | "object1": struct{}{}, 206 | "object2": struct{}{}, 207 | }, 208 | map[string]struct{}{}, 209 | }, 210 | { 211 | map[string]addRm{ 212 | "object1": addRm{rmOp, 1 * time.Minute}, 213 | "object2": addRm{rmOp, 3 * time.Minute}, 214 | }, 215 | map[string]addRm{ 216 | "object1": addRm{rmOp, 2 * time.Minute}, 217 | "object2": addRm{rmOp, 2 * time.Minute}, 218 | }, 219 | map[string]struct{}{}, 220 | map[string]struct{}{ 221 | "object1": struct{}{}, 222 | "object2": struct{}{}, 223 | }, 224 | }, 225 | } { 226 | mock1, mock2 := clock.NewMock(), clock.NewMock() 227 | 228 | lww1, err := NewLWWSet() 229 | if err != nil { 230 | t.Fatalf("unable to initialize lww set: %s", err) 231 | } 232 | lww1.clock = mock1 233 | 234 | lww2, err := NewLWWSet() 235 | if err != nil { 236 | t.Fatalf("unable to initialize lww set: %s", err) 237 | } 238 | lww2.clock = mock2 239 | 240 | var totalDuration time.Duration 241 | 242 | for obj, addrm := range tt.mapOne { 243 | curTime := addrm.d - totalDuration 244 | 245 | totalDuration += curTime 246 | mock1.Add(curTime) 247 | 248 | switch addrm.op { 249 | case addOp: 250 | lww1.Add(obj) 251 | case rmOp: 252 | lww1.Remove(obj) 253 | } 254 | } 255 | 256 | totalDuration = 0 * time.Second 257 | 258 | for obj, addrm := range tt.mapTwo { 259 | curTime := addrm.d - totalDuration 260 | 261 | totalDuration += curTime 262 | mock2.Add(curTime) 263 | 264 | switch addrm.op { 265 | case addOp: 266 | lww2.Add(obj) 267 | case rmOp: 268 | lww2.Remove(obj) 269 | } 270 | } 271 | 272 | lww1.Merge(lww2) 273 | 274 | for obj := range tt.valid { 275 | if !lww1.Contains(obj) { 276 | t.Errorf("expected merged set to contain: %q", obj) 277 | } 278 | } 279 | 280 | for obj := range tt.invalid { 281 | if lww1.Contains(obj) { 282 | t.Errorf("expected merged set to not contain: %q", obj) 283 | } 284 | } 285 | } 286 | } 287 | 288 | func TestLWWESetMarshalJSON(t *testing.T) { 289 | for _, tt := range []struct { 290 | add, rm map[interface{}]time.Duration 291 | bias BiasType 292 | expected string 293 | }{ 294 | { 295 | map[interface{}]time.Duration{}, 296 | map[interface{}]time.Duration{}, 297 | BiasAdd, 298 | `{"type":"lww-e-set","bias":"a","e":[]}`, 299 | }, 300 | { 301 | map[interface{}]time.Duration{}, 302 | map[interface{}]time.Duration{}, 303 | BiasRemove, 304 | `{"type":"lww-e-set","bias":"r","e":[]}`, 305 | }, 306 | { 307 | map[interface{}]time.Duration{ 308 | "object1": 1 * time.Second, 309 | }, 310 | map[interface{}]time.Duration{ 311 | "object2": 1 * time.Second, 312 | }, 313 | BiasAdd, 314 | `{"type":"lww-e-set","bias":"a","e":[{"el":"object1","ta":1},{"el":"object2","td":1}]}`, 315 | }, 316 | { 317 | map[interface{}]time.Duration{ 318 | "object1": 1 * time.Second, 319 | }, 320 | map[interface{}]time.Duration{ 321 | "object2": 1 * time.Second, 322 | }, 323 | BiasRemove, 324 | `{"type":"lww-e-set","bias":"r","e":[{"el":"object1","ta":1},{"el":"object2","td":1}]}`, 325 | }, 326 | { 327 | map[interface{}]time.Duration{ 328 | "object1": 1 * time.Second, 329 | }, 330 | map[interface{}]time.Duration{}, 331 | BiasAdd, 332 | `{"type":"lww-e-set","bias":"a","e":[{"el":"object1","ta":1}]}`, 333 | }, 334 | { 335 | map[interface{}]time.Duration{ 336 | "object1": 1 * time.Second, 337 | }, 338 | map[interface{}]time.Duration{}, 339 | BiasRemove, 340 | `{"type":"lww-e-set","bias":"r","e":[{"el":"object1","ta":1}]}`, 341 | }, 342 | { 343 | map[interface{}]time.Duration{}, 344 | map[interface{}]time.Duration{ 345 | "object1": 1 * time.Second, 346 | }, 347 | BiasAdd, 348 | `{"type":"lww-e-set","bias":"a","e":[{"el":"object1","td":1}]}`, 349 | }, 350 | { 351 | map[interface{}]time.Duration{}, 352 | map[interface{}]time.Duration{ 353 | "object1": 1 * time.Second, 354 | }, 355 | BiasRemove, 356 | `{"type":"lww-e-set","bias":"r","e":[{"el":"object1","td":1}]}`, 357 | }, 358 | { 359 | map[interface{}]time.Duration{ 360 | "object1": 1 * time.Second, 361 | }, 362 | map[interface{}]time.Duration{ 363 | "object1": 2 * time.Second, 364 | }, 365 | BiasAdd, 366 | `{"type":"lww-e-set","bias":"a","e":[{"el":"object1","ta":1,"td":2}]}`, 367 | }, 368 | { 369 | map[interface{}]time.Duration{ 370 | "object1": 1 * time.Second, 371 | }, 372 | map[interface{}]time.Duration{ 373 | "object1": 2 * time.Second, 374 | }, 375 | BiasRemove, 376 | `{"type":"lww-e-set","bias":"r","e":[{"el":"object1","ta":1,"td":2}]}`, 377 | }, 378 | } { 379 | lww, err := NewLWWSetWithBias(tt.bias) 380 | if err != nil { 381 | t.Fatalf("unexpected error creating new LWW-E-Set: %s", err) 382 | } 383 | 384 | mock := clock.NewMock() 385 | lww.clock = mock 386 | 387 | for e, d := range tt.add { 388 | mock.Add(d) 389 | lww.Add(e) 390 | 391 | mock = clock.NewMock() 392 | lww.clock = mock 393 | } 394 | 395 | for e, d := range tt.rm { 396 | mock.Add(d) 397 | lww.Remove(e) 398 | 399 | mock = clock.NewMock() 400 | lww.clock = mock 401 | } 402 | 403 | out, err := json.Marshal(lww) 404 | if err != nil { 405 | t.Fatalf("unexpected error on marshalling: %s", err) 406 | } 407 | 408 | if !bytes.Equal([]byte(tt.expected), out) { 409 | t.Errorf("expected marshalling array to be: %q, actual: %q", tt.expected, out) 410 | } 411 | } 412 | } 413 | -------------------------------------------------------------------------------- /or_set.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "github.com/satori/go.uuid" 4 | 5 | type ORSet struct { 6 | addMap map[interface{}]map[string]struct{} 7 | rmMap map[interface{}]map[string]struct{} 8 | } 9 | 10 | func NewORSet() *ORSet { 11 | return &ORSet{ 12 | addMap: make(map[interface{}]map[string]struct{}), 13 | rmMap: make(map[interface{}]map[string]struct{}), 14 | } 15 | } 16 | 17 | func (o *ORSet) Add(value interface{}) { 18 | if m, ok := o.addMap[value]; ok { 19 | m[uuid.NewV4().String()] = struct{}{} 20 | o.addMap[value] = m 21 | return 22 | } 23 | 24 | m := make(map[string]struct{}) 25 | 26 | m[uuid.NewV4().String()] = struct{}{} 27 | o.addMap[value] = m 28 | } 29 | 30 | func (o *ORSet) Remove(value interface{}) { 31 | r, ok := o.rmMap[value] 32 | if !ok { 33 | r = make(map[string]struct{}) 34 | } 35 | 36 | if m, ok := o.addMap[value]; ok { 37 | for uid, _ := range m { 38 | r[uid] = struct{}{} 39 | } 40 | } 41 | 42 | o.rmMap[value] = r 43 | } 44 | 45 | func (o *ORSet) Contains(value interface{}) bool { 46 | addMap, ok := o.addMap[value] 47 | if !ok { 48 | return false 49 | } 50 | 51 | rmMap, ok := o.rmMap[value] 52 | if !ok { 53 | return true 54 | } 55 | 56 | for uid, _ := range addMap { 57 | if _, ok := rmMap[uid]; !ok { 58 | return true 59 | } 60 | } 61 | 62 | return false 63 | } 64 | 65 | func (o *ORSet) Merge(r *ORSet) { 66 | for value, m := range r.addMap { 67 | addMap, ok := o.addMap[value] 68 | if ok { 69 | for uid, _ := range m { 70 | addMap[uid] = struct{}{} 71 | } 72 | 73 | continue 74 | } 75 | 76 | o.addMap[value] = m 77 | } 78 | 79 | for value, m := range r.rmMap { 80 | rmMap, ok := o.rmMap[value] 81 | if ok { 82 | for uid, _ := range m { 83 | rmMap[uid] = struct{}{} 84 | } 85 | 86 | continue 87 | } 88 | 89 | o.rmMap[value] = m 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /or_set_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "testing" 4 | 5 | func TestORSetAddContains(t *testing.T) { 6 | orSet := NewORSet() 7 | 8 | var testValue string = "object" 9 | 10 | if orSet.Contains(testValue) { 11 | t.Errorf("Expected set to not contain: %v, but found", testValue) 12 | } 13 | 14 | orSet.Add(testValue) 15 | 16 | if !orSet.Contains(testValue) { 17 | t.Errorf("Expected set to contain: %v, but not found", testValue) 18 | } 19 | } 20 | 21 | func TestORSetAddRemoveContains(t *testing.T) { 22 | orSet := NewORSet() 23 | 24 | var testValue string = "object" 25 | orSet.Add(testValue) 26 | 27 | orSet.Remove(testValue) 28 | 29 | if orSet.Contains(testValue) { 30 | t.Errorf("Expected set to not contain: %v, but found", testValue) 31 | } 32 | } 33 | 34 | func TestORSetAddRemoveAddContains(t *testing.T) { 35 | orSet := NewORSet() 36 | 37 | var testValue string = "object" 38 | 39 | orSet.Add(testValue) 40 | orSet.Remove(testValue) 41 | orSet.Add(testValue) 42 | 43 | if !orSet.Contains(testValue) { 44 | t.Errorf("Expected set to contain: %v, but not found", testValue) 45 | } 46 | } 47 | 48 | func TestORSetAddAddRemoveContains(t *testing.T) { 49 | orSet := NewORSet() 50 | 51 | var testValue string = "object" 52 | 53 | orSet.Add(testValue) 54 | orSet.Add(testValue) 55 | orSet.Remove(testValue) 56 | 57 | if orSet.Contains(testValue) { 58 | t.Errorf("Expected set to not contain: %v, but found", testValue) 59 | } 60 | } 61 | 62 | func TestORSetMerge(t *testing.T) { 63 | type addRm struct { 64 | addSet []string 65 | rmSet []string 66 | } 67 | 68 | for _, tt := range []struct { 69 | setOne addRm 70 | setTwo addRm 71 | valid map[string]struct{} 72 | invalid map[string]struct{} 73 | }{ 74 | { 75 | addRm{[]string{"object1"}, []string{}}, 76 | addRm{[]string{}, []string{"object1"}}, 77 | map[string]struct{}{ 78 | "object1": struct{}{}, 79 | }, 80 | map[string]struct{}{}, 81 | }, 82 | { 83 | addRm{[]string{}, []string{"object1"}}, 84 | addRm{[]string{"object1"}, []string{}}, 85 | map[string]struct{}{ 86 | "object1": struct{}{}, 87 | }, 88 | map[string]struct{}{}, 89 | }, 90 | { 91 | addRm{[]string{"object1"}, []string{"object1"}}, 92 | addRm{[]string{}, []string{}}, 93 | map[string]struct{}{}, 94 | map[string]struct{}{ 95 | "object1": struct{}{}, 96 | }, 97 | }, 98 | { 99 | addRm{[]string{}, []string{}}, 100 | addRm{[]string{"object1"}, []string{"object1"}}, 101 | map[string]struct{}{}, 102 | map[string]struct{}{ 103 | "object1": struct{}{}, 104 | }, 105 | }, 106 | { 107 | addRm{[]string{"object2"}, []string{"object1"}}, 108 | addRm{[]string{"object1"}, []string{"object2"}}, 109 | map[string]struct{}{ 110 | "object1": struct{}{}, 111 | "object2": struct{}{}, 112 | }, 113 | map[string]struct{}{}, 114 | }, 115 | { 116 | addRm{[]string{"object2", "object1"}, []string{"object1"}}, 117 | addRm{[]string{"object1", "object2"}, []string{"object2"}}, 118 | map[string]struct{}{ 119 | "object1": struct{}{}, 120 | "object2": struct{}{}, 121 | }, 122 | map[string]struct{}{}, 123 | }, 124 | { 125 | addRm{[]string{"object2", "object1"}, []string{"object1", "object2"}}, 126 | addRm{[]string{"object1", "object2"}, []string{"object2", "object1"}}, 127 | map[string]struct{}{}, 128 | map[string]struct{}{ 129 | "object1": struct{}{}, 130 | "object2": struct{}{}, 131 | }, 132 | }, 133 | } { 134 | orset1, orset2 := NewORSet(), NewORSet() 135 | 136 | for _, add := range tt.setOne.addSet { 137 | orset1.Add(add) 138 | } 139 | 140 | for _, rm := range tt.setOne.rmSet { 141 | orset1.Remove(rm) 142 | } 143 | 144 | for _, add := range tt.setTwo.addSet { 145 | orset2.Add(add) 146 | } 147 | 148 | for _, rm := range tt.setTwo.rmSet { 149 | orset2.Remove(rm) 150 | } 151 | 152 | orset1.Merge(orset2) 153 | 154 | for obj, _ := range tt.valid { 155 | if !orset1.Contains(obj) { 156 | t.Errorf("expected set to contain: %v", obj) 157 | } 158 | } 159 | 160 | for obj, _ := range tt.invalid { 161 | if orset1.Contains(obj) { 162 | t.Errorf("expected set to not contain: %v", obj) 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /pn_counter.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | // PNCounter represents a state-based PN-Counter. It is 4 | // implemented as sets of two G-Counters, one that tracks 5 | // increments while the other decrements. 6 | type PNCounter struct { 7 | pCounter *GCounter 8 | nCounter *GCounter 9 | } 10 | 11 | // NewPNCounter returns a new *PNCounter with both its 12 | // G-Counters initialized. 13 | func NewPNCounter() *PNCounter { 14 | return &PNCounter{ 15 | pCounter: NewGCounter(), 16 | nCounter: NewGCounter(), 17 | } 18 | } 19 | 20 | // Inc monotonically increments the current value of the 21 | // PN-Counter by one. 22 | func (pn *PNCounter) Inc() { 23 | pn.IncVal(1) 24 | } 25 | 26 | // IncVal increments the current value of the PN-Counter 27 | // by the delta incr that is provided. The value of delta 28 | // has to be >= 0. If the value of delta is < 0, then this 29 | // implementation panics. 30 | func (pn *PNCounter) IncVal(incr int) { 31 | pn.pCounter.IncVal(incr) 32 | } 33 | 34 | // Dec monotonically decrements the current value of the 35 | // PN-Counter by one. 36 | func (pn *PNCounter) Dec() { 37 | pn.DecVal(1) 38 | } 39 | 40 | // DecVal adds a decrement to the current value of 41 | // PN-Counter by the value of delta decr. Similar to 42 | // IncVal, the value of decr cannot be less than 0. 43 | func (pn *PNCounter) DecVal(decr int) { 44 | pn.nCounter.IncVal(decr) 45 | } 46 | 47 | // Count returns the current value of the counter. It 48 | // subtracts the value of negative G-Counter from the 49 | // positive grow-only counter and returns the result. 50 | // Because this counter can grow in either direction, 51 | // negative integers as results are possible. 52 | func (pn *PNCounter) Count() int { 53 | return pn.pCounter.Count() - pn.nCounter.Count() 54 | } 55 | 56 | // Merge combines both the PN-Counters and saves the result 57 | // in the invoking counter. Respective G-Counters are merged 58 | // i.e. +ve with +ve and -ve with -ve, but not computation 59 | // is actually performed. 60 | func (pn *PNCounter) Merge(pnpn *PNCounter) { 61 | pn.pCounter.Merge(pnpn.pCounter) 62 | pn.nCounter.Merge(pnpn.nCounter) 63 | } 64 | -------------------------------------------------------------------------------- /pn_counter_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "testing" 4 | 5 | func TestPNCounter(t *testing.T) { 6 | for _, tt := range []struct { 7 | incOne, decOne int 8 | incTwo, decTwo int 9 | 10 | result int 11 | }{ 12 | {5, 5, 6, 6, 0}, 13 | {5, 6, 7, 8, -2}, 14 | {8, 7, 6, 5, 2}, 15 | {5, 0, 6, 0, 11}, 16 | {0, 5, 0, 6, -11}, 17 | } { 18 | pOne, pTwo := NewPNCounter(), NewPNCounter() 19 | 20 | for i := 0; i < tt.incOne; i++ { 21 | pOne.Inc() 22 | } 23 | 24 | for i := 0; i < tt.decOne; i++ { 25 | pOne.Dec() 26 | } 27 | 28 | for i := 0; i < tt.incTwo; i++ { 29 | pTwo.Inc() 30 | } 31 | 32 | for i := 0; i < tt.decTwo; i++ { 33 | pTwo.Dec() 34 | } 35 | 36 | pOne.Merge(pTwo) 37 | 38 | if pOne.Count() != tt.result { 39 | t.Errorf("expected the total count to be: %d, actual: %d", 40 | tt.result, 41 | pOne.Count()) 42 | } 43 | 44 | pTwo.Merge(pOne) 45 | 46 | if pTwo.Count() != tt.result { 47 | t.Errorf("expected the total count to be: %d, actual: %d", 48 | tt.result, 49 | pTwo.Count()) 50 | } 51 | } 52 | } 53 | 54 | func TestPNCounterInvalidP(t *testing.T) { 55 | pn := NewPNCounter() 56 | 57 | defer func() { 58 | if r := recover(); r == nil { 59 | t.Fatalf("panic expected here") 60 | } 61 | }() 62 | 63 | pn.IncVal(-5) 64 | } 65 | 66 | func TestPNCounterInvalidN(t *testing.T) { 67 | pn := NewPNCounter() 68 | 69 | defer func() { 70 | if r := recover(); r == nil { 71 | t.Fatalf("panic expected here") 72 | } 73 | }() 74 | 75 | pn.DecVal(-5) 76 | } 77 | -------------------------------------------------------------------------------- /set.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | type Set interface { 4 | Add(interface{}) 5 | Contains(interface{}) bool 6 | } 7 | -------------------------------------------------------------------------------- /twophase_set.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import "encoding/json" 4 | 5 | var ( 6 | // TwoPhaseSet should implement Set. 7 | _ Set = &TwoPhaseSet{} 8 | ) 9 | 10 | // TwoPhaseSet supports both addition and removal of 11 | // elements to set. 12 | type TwoPhaseSet struct { 13 | addSet *GSet 14 | rmSet *GSet 15 | } 16 | 17 | // NewTwoPhaseSet returns a new instance of TwoPhaseSet. 18 | func NewTwoPhaseSet() *TwoPhaseSet { 19 | return &TwoPhaseSet{ 20 | addSet: NewGSet(), 21 | rmSet: NewGSet(), 22 | } 23 | } 24 | 25 | // Add inserts element into the TwoPhaseSet. 26 | func (t *TwoPhaseSet) Add(elem interface{}) { 27 | t.addSet.Add(elem) 28 | } 29 | 30 | // Remove deletes the element from the set. 31 | func (t *TwoPhaseSet) Remove(elem interface{}) { 32 | t.rmSet.Add(elem) 33 | } 34 | 35 | // Contains returns true if the set contains the element. 36 | // The set is said to contain the element if it is present 37 | // in the add-set and not in the remove-set. 38 | func (t *TwoPhaseSet) Contains(elem interface{}) bool { 39 | return t.addSet.Contains(elem) && !t.rmSet.Contains(elem) 40 | } 41 | 42 | type tpsetJSON struct { 43 | T string `json:"type"` 44 | A []interface{} `json:"a"` 45 | R []interface{} `json:"r"` 46 | } 47 | 48 | // MarshalJSON serializes TwoPhaseSet into an JSON array. 49 | func (t *TwoPhaseSet) MarshalJSON() ([]byte, error) { 50 | return json.Marshal(&tpsetJSON{ 51 | T: "2p-set", 52 | A: t.addSet.Elems(), 53 | R: t.rmSet.Elems(), 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /twophase_set_test.go: -------------------------------------------------------------------------------- 1 | package crdt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | ) 8 | 9 | func TestTwoPhaseSetAdd(t *testing.T) { 10 | tpset := NewTwoPhaseSet() 11 | elem := "some-test-element" 12 | 13 | if tpset.Contains(elem) { 14 | t.Errorf("set should not contain %q, not yet added", elem) 15 | } 16 | 17 | tpset.Add(elem) 18 | if !tpset.Contains(elem) { 19 | t.Errorf("set should contain %q", elem) 20 | } 21 | } 22 | 23 | func TestTwoPhaseSetAddRemove(t *testing.T) { 24 | for _, tt := range []struct { 25 | elem interface{} 26 | fnAddRemove func(*TwoPhaseSet, interface{}) 27 | }{ 28 | { 29 | "test-element", 30 | 31 | // First add the element and remove it. As a result 32 | // of this the set should not contain that element. 33 | func(tp *TwoPhaseSet, obj interface{}) { 34 | tp.Add(obj) 35 | tp.Remove(obj) 36 | }, 37 | }, 38 | { 39 | "test-element", 40 | 41 | // First remove the element and add it. Because of the 42 | // commutative property of this set the outcome should 43 | // still be the set that doesn't contain the element. 44 | func(tp *TwoPhaseSet, obj interface{}) { 45 | tp.Remove(obj) 46 | tp.Add(obj) 47 | }, 48 | }, 49 | } { 50 | tpset := NewTwoPhaseSet() 51 | 52 | if tpset.Contains(tt.elem) { 53 | t.Errorf("set should not contain elem %q", tt.elem) 54 | } 55 | 56 | tt.fnAddRemove(tpset, tt.elem) 57 | 58 | if tpset.Contains(tt.elem) { 59 | t.Errorf("set should not contain elem %q", tt.elem) 60 | } 61 | } 62 | } 63 | 64 | func TestTwoPhaseSetMarshalJSON(t *testing.T) { 65 | for _, tt := range []struct { 66 | add, rm []interface{} 67 | expected string 68 | }{ 69 | {[]interface{}{}, []interface{}{}, `{"type":"2p-set","a":[],"r":[]}`}, 70 | {[]interface{}{"alpha"}, []interface{}{}, `{"type":"2p-set","a":["alpha"],"r":[]}`}, 71 | {[]interface{}{}, []interface{}{"beta"}, `{"type":"2p-set","a":[],"r":["beta"]}`}, 72 | {[]interface{}{"alpha"}, []interface{}{"beta"}, `{"type":"2p-set","a":["alpha"],"r":["beta"]}`}, 73 | {[]interface{}{"alpha"}, []interface{}{"alpha"}, `{"type":"2p-set","a":["alpha"],"r":["alpha"]}`}, 74 | 75 | {[]interface{}{1}, []interface{}{}, `{"type":"2p-set","a":[1],"r":[]}`}, 76 | {[]interface{}{}, []interface{}{2}, `{"type":"2p-set","a":[],"r":[2]}`}, 77 | {[]interface{}{1}, []interface{}{2}, `{"type":"2p-set","a":[1],"r":[2]}`}, 78 | {[]interface{}{1}, []interface{}{1}, `{"type":"2p-set","a":[1],"r":[1]}`}, 79 | } { 80 | tpset := NewTwoPhaseSet() 81 | 82 | for _, e := range tt.add { 83 | tpset.Add(e) 84 | } 85 | 86 | for _, e := range tt.rm { 87 | tpset.Remove(e) 88 | } 89 | 90 | out, err := json.Marshal(tpset) 91 | if err != nil { 92 | t.Fatalf("unexpected error marshalling tpset: %s", err) 93 | } 94 | 95 | if !bytes.Equal([]byte(tt.expected), out) { 96 | t.Errorf("expected marshalled bytes: %q, actual: %q", tt.expected, out) 97 | } 98 | } 99 | } 100 | --------------------------------------------------------------------------------