diff --git a/network/fingerprints.go b/network/fingerprints.go new file mode 100644 index 0000000000000000000000000000000000000000..4c44331bb2bc9427a11981985c84e38e9012615c --- /dev/null +++ b/network/fingerprints.go @@ -0,0 +1,131 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package network + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/primitives/format" + "sync" +) + +// Processor is an object which ties an interfaces.MessageProcessorFP +// to a lock. This prevents the processor from being used in multiple +// different threads. +type Processor struct { + interfaces.MessageProcessorFP + sync.Mutex +} + +// NewFingerprints is a constructor function for the Processor object. +func newProcessor(mp interfaces.MessageProcessorFP) *Processor { + return &Processor{ + MessageProcessorFP: mp, + Mutex: sync.Mutex{}, + } +} + +// Fingerprints is a thread-safe map, mapping format.Fingerprint's to +// a Processor object. +type Fingerprints struct { + fingerprints map[format.Fingerprint]*Processor + sync.RWMutex +} + +// NewFingerprints is a constructor function for the Fingerprints tracker. +func NewFingerprints() *Fingerprints { + return &Fingerprints{ + fingerprints: make(map[format.Fingerprint]*Processor), + RWMutex: sync.RWMutex{}, + } +} + +// Get is a thread-safe getter for the Fingerprints map. Get returns the mapped +// processor and true (representing that it exists in the map) if the provided +// fingerprint has an entry. Otherwise, Get returns nil and false. +func (f *Fingerprints) Get(fingerprint format.Fingerprint) (*Processor, bool) { + f.RLock() + defer f.RUnlock() + fp, exists := f.fingerprints[fingerprint] + if !exists { + return nil, false + } + return fp, true +} + +// AddFingerprint is a thread-safe setter for the Fingerprints map. AddFingerprint +// maps the given fingerprint key to the processor value. If there is already +// an entry for this fingerprint, the method returns with no write operation. +func (f *Fingerprints) AddFingerprint(fingerprint format.Fingerprint, + processor interfaces.MessageProcessorFP) { + f.Lock() + defer f.Unlock() + + f.addFingerprint(fingerprint, processor) + +} + +// AddFingerprints is a thread-safe setter for multiple entries into +// the Fingerprints map. If there is not a 1:1 relationship between +// fingerprints and processors slices (i.e. the lengths of these slices +// are equivalent), an error will be returned. +// Otherwise, each fingerprint is written to the associated processor. +// If there is already an entry for the given fingerprint/processor pair, +// no write operation will occur for this pair. +func (f *Fingerprints) AddFingerprints(fps []format.Fingerprint, + processors []interfaces.MessageProcessorFP) error { + f.Lock() + defer f.Unlock() + + if len(fps) != len(processors) { + return errors.Errorf("Canot perform a batch add when there are "+ + "not an equal amount of fingerprints and processors. "+ + "Given %d fingerprints and %d processors.", len(fps), len(processors)) + } + + for i, fp := range fps { + f.addFingerprint(fp, processors[i]) + } + + return nil +} + +// addFingerprint is a non-thread-safe helper function which writes a Processor +// to the given fingerprint key. If an entry already exists for this fingerprint key, +// no write operation occurs. +func (f *Fingerprints) addFingerprint(fingerprint format.Fingerprint, + processor interfaces.MessageProcessorFP) { + + if _, exists := f.fingerprints[fingerprint]; exists { + return + } + + newMsgProc := newProcessor(processor) + + f.fingerprints[fingerprint] = newMsgProc +} + +// RemoveFingerprint is a thread-safe deletion operation on the Fingerprints map. +// It will remove the entry for the given fingerprint from the map. +func (f *Fingerprints) RemoveFingerprint(fingerprint format.Fingerprint) { + f.Lock() + defer f.Unlock() + + delete(f.fingerprints, fingerprint) +} + +// RemoveFingerprints is a thread-safe batch deletion operation on the Fingerprints map. +// It will remove the entries for the given fingerprints from the map. +func (f *Fingerprints) RemoveFingerprints(fingerprint []format.Fingerprint) { + f.Lock() + defer f.Unlock() + + for _, fp := range fingerprint { + delete(f.fingerprints, fp) + } +} diff --git a/network/fingerprints_test.go b/network/fingerprints_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6a688ec3410a0a0067a1acbaadebbff06694d249 --- /dev/null +++ b/network/fingerprints_test.go @@ -0,0 +1,240 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package network + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/primitives/format" + "reflect" + "strconv" + "sync" + "testing" +) + +// Unit test. +func TestNewFingerprints(t *testing.T) { + expected := &Fingerprints{ + fingerprints: make(map[format.Fingerprint]*Processor), + RWMutex: sync.RWMutex{}, + } + + received := NewFingerprints() + + if !reflect.DeepEqual(expected, received) { + t.Fatalf("NewFingerprint error: Did not construct expected object."+ + "\nExpected: %v"+ + "\nReceived: %v", expected, received) + } +} + +// Unit test. +func TestFingerprints_Get(t *testing.T) { + // Construct fingerprint map + fpTracker := NewFingerprints() + + // Construct fingerprint and processor values + fp := format.NewFingerprint([]byte("test")) + mp := NewMockMsgProcessor(t) + + // Add the values to the tracker + fpTracker.AddFingerprint(fp, mp) + + // Attempt to retrieve value from map + received, exists := fpTracker.Get(fp) + if !exists { + t.Fatalf("Get error: Did not retrieve fingerprint (%s) that "+ + "should have been in map.", fp) + } + + // Check that received value contains the expected data + expected := newProcessor(mp) + if !reflect.DeepEqual(received, expected) { + t.Fatalf("Get error: Map does not contain expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", expected, received) + } + +} + +// Unit test. +func TestFingerprints_AddFingerprint(t *testing.T) { + // Construct fingerprint map + fpTracker := NewFingerprints() + + // Construct fingerprint and processor values + fp := format.NewFingerprint([]byte("test")) + mp := NewMockMsgProcessor(t) + + // Add the values to the tracker + fpTracker.AddFingerprint(fp, mp) + + // Check that the fingerprint key has a map entry + received, exists := fpTracker.fingerprints[fp] + if !exists { + t.Fatalf("AddFingerprint did not write to map as expected. "+ + "Fingerprint %s not found in map", fp) + } + + // Check that received value contains the expected data + expected := newProcessor(mp) + if !reflect.DeepEqual(received, expected) { + t.Fatalf("AddFingerprint error: Map does not contain expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", expected, received) + } +} + +// Unit test. +func TestFingerprints_AddFingerprints(t *testing.T) { + // Construct fingerprints map + fpTracker := NewFingerprints() + + // Construct slices of fingerprints and processors + numTests := 100 + fingerprints := make([]format.Fingerprint, 0, numTests) + processors := make([]interfaces.MessageProcessorFP, 0, numTests) + for i := 0; i < numTests; i++ { + fp := format.NewFingerprint([]byte(strconv.Itoa(i))) + mp := NewMockMsgProcessor(t) + + fingerprints = append(fingerprints, fp) + processors = append(processors, mp) + } + + // Add slices to map + err := fpTracker.AddFingerprints(fingerprints, processors) + if err != nil { + t.Fatalf("AddFingerprints unexpected error: %v", err) + } + + // Make sure every fingerprint is mapped to it's expected processor + for i, expected := range fingerprints { + received, exists := fpTracker.fingerprints[expected] + if !exists { + t.Errorf("AddFingerprints did not write to map as expected. "+ + "Fingerprint number %d (value: %s) not found in map", i, expected) + } + + if !reflect.DeepEqual(received, expected) { + t.Fatalf("AddFingerprints error: Map does not contain expected data for "+ + "fingerprint number %d."+ + "\nExpected: %v"+ + "\nReceived: %v", i, expected, received) + } + } + +} + +// Error case: Call Fingerprints.AddFingerprints with fingerprint and processor +// slices of different lengths. +func TestFingerprints_AddFingerprints_Error(t *testing.T) { + // Construct fingerprint map + fpTracker := NewFingerprints() + + // Construct 2 slices of different lengths + fingerprints := []format.Fingerprint{ + format.NewFingerprint([]byte("1")), + format.NewFingerprint([]byte("2")), + format.NewFingerprint([]byte("3")), + } + processors := []interfaces.MessageProcessorFP{ + NewMockMsgProcessor(t), + } + + // Attempt to add fingerprints + err := fpTracker.AddFingerprints(fingerprints, processors) + if err == nil { + t.Fatalf("AddFingerprints should have received an error with mismatched " + + "slices length") + } + +} + +func TestFingerprints_RemoveFingerprint(t *testing.T) { + + // Construct fingerprint map + fpTracker := NewFingerprints() + + // Construct fingerprint and processor values + fp := format.NewFingerprint([]byte("test")) + mp := NewMockMsgProcessor(t) + + // Add the values to the tracker + fpTracker.AddFingerprint(fp, mp) + + // Remove value from tracker + fpTracker.RemoveFingerprint(fp) + + // Check that value no longer exists within the map + if _, exists := fpTracker.fingerprints[fp]; exists { + t.Fatalf("RemoveFingerprint error: "+ + "Fingerprint %s exists in map after a RemoveFingerprint call", fp) + } +} + +// Unit test. +func TestFingerprints_RemoveFingerprints(t *testing.T) { + // Construct fingerprints map + fpTracker := NewFingerprints() + + // Construct slices of fingerprints and processors + numTests := 100 + fingerprints := make([]format.Fingerprint, 0, numTests) + processors := make([]interfaces.MessageProcessorFP, 0, numTests) + for i := 0; i < numTests; i++ { + fp := format.NewFingerprint([]byte(strconv.Itoa(i))) + mp := NewMockMsgProcessor(t) + + fingerprints = append(fingerprints, fp) + processors = append(processors, mp) + } + + // Add slices to map + err := fpTracker.AddFingerprints(fingerprints, processors) + if err != nil { + t.Fatalf("AddFingerprints unexpected error: %v", err) + } + + fpTracker.RemoveFingerprints(fingerprints) + + // Make sure every fingerprint is mapped to it's expected processor + for i, expected := range fingerprints { + + if received, exists := fpTracker.fingerprints[expected]; !exists { + t.Fatalf("RemoveFingerprints error: Map does not contain "+ + "expected data for fingerprint number %d."+ + "\nExpected: %v"+ + "\nReceived: %v", i, expected, received) + } + + } + +} + +// todo: consider moving this to a test utils somewhere else.. maybe in the interfaces package? +type MockMsgProcessor struct{} + +func NewMockMsgProcessor(face interface{}) *MockMsgProcessor { + switch face.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + jww.FATAL.Panicf("NewMockMsgProcessor is restricted to testing only. Got %T", face) + } + + return &MockMsgProcessor{} +} + +func (mock *MockMsgProcessor) MarkFingerprintUsed(fingerprint format.Fingerprint) { + return +} + +func (mock *MockMsgProcessor) Process(message format.Message, fingerprint format.Fingerprint) { + return +}