diff --git a/atomic.go b/atomic.go deleted file mode 100644 index 8bdf5d7..0000000 --- a/atomic.go +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) 2016-2020 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -// Package atomic provides simple wrappers around numerics to enforce atomic -// access. -package atomic - -import ( - "encoding/json" - "math" - "strconv" - "sync/atomic" - "time" -) - -// Bool is an atomic Boolean. -type Bool struct { - nocmp // disallow non-atomic comparison - - v uint32 -} - -// NewBool creates a Bool. -func NewBool(initial bool) *Bool { - return &Bool{v: boolToInt(initial)} -} - -// Load atomically loads the Boolean. -func (b *Bool) Load() bool { - return truthy(atomic.LoadUint32(&b.v)) -} - -// CAS is an atomic compare-and-swap. -func (b *Bool) CAS(old, new bool) bool { - return atomic.CompareAndSwapUint32(&b.v, boolToInt(old), boolToInt(new)) -} - -// Store atomically stores the passed value. -func (b *Bool) Store(new bool) { - atomic.StoreUint32(&b.v, boolToInt(new)) -} - -// Swap sets the given value and returns the previous value. -func (b *Bool) Swap(new bool) bool { - return truthy(atomic.SwapUint32(&b.v, boolToInt(new))) -} - -// Toggle atomically negates the Boolean and returns the previous value. -func (b *Bool) Toggle() bool { - for { - old := b.Load() - if b.CAS(old, !old) { - return old - } - } -} - -func truthy(n uint32) bool { - return n == 1 -} - -func boolToInt(b bool) uint32 { - if b { - return 1 - } - return 0 -} - -// MarshalJSON encodes the wrapped bool into JSON. -func (b *Bool) MarshalJSON() ([]byte, error) { - return json.Marshal(b.Load()) -} - -// UnmarshalJSON decodes JSON into the wrapped bool. -func (b *Bool) UnmarshalJSON(t []byte) error { - var v bool - if err := json.Unmarshal(t, &v); err != nil { - return err - } - b.Store(v) - return nil -} - -// String encodes the wrapped value as a string. -func (b *Bool) String() string { - return strconv.FormatBool(b.Load()) -} - -// Float64 is an atomic wrapper around float64. -type Float64 struct { - nocmp // disallow non-atomic comparison - - v uint64 -} - -// NewFloat64 creates a Float64. -func NewFloat64(f float64) *Float64 { - return &Float64{v: math.Float64bits(f)} -} - -// Load atomically loads the wrapped value. -func (f *Float64) Load() float64 { - return math.Float64frombits(atomic.LoadUint64(&f.v)) -} - -// Store atomically stores the passed value. -func (f *Float64) Store(s float64) { - atomic.StoreUint64(&f.v, math.Float64bits(s)) -} - -// Add atomically adds to the wrapped float64 and returns the new value. -func (f *Float64) Add(s float64) float64 { - for { - old := f.Load() - new := old + s - if f.CAS(old, new) { - return new - } - } -} - -// Sub atomically subtracts from the wrapped float64 and returns the new value. -func (f *Float64) Sub(s float64) float64 { - return f.Add(-s) -} - -// CAS is an atomic compare-and-swap. -func (f *Float64) CAS(old, new float64) bool { - return atomic.CompareAndSwapUint64(&f.v, math.Float64bits(old), math.Float64bits(new)) -} - -// MarshalJSON encodes the wrapped float64 into JSON. -func (f *Float64) MarshalJSON() ([]byte, error) { - return json.Marshal(f.Load()) -} - -// UnmarshalJSON decodes JSON into the wrapped float64. -func (f *Float64) UnmarshalJSON(b []byte) error { - var v float64 - if err := json.Unmarshal(b, &v); err != nil { - return err - } - f.Store(v) - return nil -} - -// String encodes the wrapped value as a string. -func (f *Float64) String() string { - // 'g' is the behavior for floats with %v. - return strconv.FormatFloat(f.Load(), 'g', -1, 64) -} - -// Duration is an atomic wrapper around time.Duration -// https://godoc.org/time#Duration -type Duration struct { - nocmp // disallow non-atomic comparison - - v Int64 -} - -// NewDuration creates a Duration. -func NewDuration(d time.Duration) *Duration { - return &Duration{v: *NewInt64(int64(d))} -} - -// Load atomically loads the wrapped value. -func (d *Duration) Load() time.Duration { - return time.Duration(d.v.Load()) -} - -// Store atomically stores the passed value. -func (d *Duration) Store(n time.Duration) { - d.v.Store(int64(n)) -} - -// Add atomically adds to the wrapped time.Duration and returns the new value. -func (d *Duration) Add(n time.Duration) time.Duration { - return time.Duration(d.v.Add(int64(n))) -} - -// Sub atomically subtracts from the wrapped time.Duration and returns the new value. -func (d *Duration) Sub(n time.Duration) time.Duration { - return time.Duration(d.v.Sub(int64(n))) -} - -// Swap atomically swaps the wrapped time.Duration and returns the old value. -func (d *Duration) Swap(n time.Duration) time.Duration { - return time.Duration(d.v.Swap(int64(n))) -} - -// CAS is an atomic compare-and-swap. -func (d *Duration) CAS(old, new time.Duration) bool { - return d.v.CAS(int64(old), int64(new)) -} - -// MarshalJSON encodes the wrapped time.Duration into JSON. -func (d *Duration) MarshalJSON() ([]byte, error) { - return json.Marshal(d.Load()) -} - -// UnmarshalJSON decodes JSON into the wrapped time.Duration. -func (d *Duration) UnmarshalJSON(b []byte) error { - var v time.Duration - if err := json.Unmarshal(b, &v); err != nil { - return err - } - d.Store(v) - return nil -} - -// String encodes the wrapped value as a string. -func (d *Duration) String() string { - return d.Load().String() -} - -// Value shadows the type of the same name from sync/atomic -// https://godoc.org/sync/atomic#Value -type Value struct { - nocmp // disallow non-atomic comparison - atomic.Value -} diff --git a/atomic_test.go b/atomic_test.go deleted file mode 100644 index cb3768e..0000000 --- a/atomic_test.go +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) 2016-2020 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - -package atomic - -import ( - "encoding/json" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBool(t *testing.T) { - atom := NewBool(false) - require.False(t, atom.Toggle(), "Expected Toggle to return previous value.") - require.True(t, atom.Toggle(), "Expected Toggle to return previous value.") - require.False(t, atom.Toggle(), "Expected Toggle to return previous value.") - require.True(t, atom.Load(), "Unexpected state after swap.") - - require.True(t, atom.CAS(true, true), "CAS should swap when old matches") - require.True(t, atom.Load(), "CAS should have no effect") - require.True(t, atom.CAS(true, false), "CAS should swap when old matches") - require.False(t, atom.Load(), "CAS should have modified the value") - require.False(t, atom.CAS(true, false), "CAS should fail on old mismatch") - require.False(t, atom.Load(), "CAS should not have modified the value") - - atom.Store(false) - require.False(t, atom.Load(), "Unexpected state after store.") - - prev := atom.Swap(false) - require.False(t, prev, "Expected Swap to return previous value.") - - prev = atom.Swap(true) - require.False(t, prev, "Expected Swap to return previous value.") - - t.Run("JSON/Marshal", func(t *testing.T) { - atom.Store(true) - bytes, err := json.Marshal(atom) - require.NoError(t, err, "json.Marshal errored unexpectedly.") - require.Equal(t, []byte("true"), bytes, "json.Marshal encoded the wrong bytes.") - }) - - t.Run("JSON/Unmarshal", func(t *testing.T) { - err := json.Unmarshal([]byte("false"), &atom) - require.NoError(t, err, "json.Unmarshal errored unexpectedly.") - require.False(t, atom.Load(), "json.Unmarshal didn't set the correct value.") - }) - - t.Run("JSON/Unmarshal/Error", func(t *testing.T) { - err := json.Unmarshal([]byte("42"), &atom) - require.Error(t, err, "json.Unmarshal didn't error as expected.") - assertErrorJSONUnmarshalType(t, err, - "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) - }) - - t.Run("String", func(t *testing.T) { - t.Run("true", func(t *testing.T) { - assert.Equal(t, "true", NewBool(true).String(), - "String() returned an unexpected value.") - }) - - t.Run("false", func(t *testing.T) { - var b Bool - assert.Equal(t, "false", b.String(), - "String() returned an unexpected value.") - }) - }) -} - -func TestFloat64(t *testing.T) { - atom := NewFloat64(4.2) - - require.Equal(t, float64(4.2), atom.Load(), "Load didn't work.") - - require.True(t, atom.CAS(4.2, 0.5), "CAS didn't report a swap.") - require.Equal(t, float64(0.5), atom.Load(), "CAS didn't set the correct value.") - require.False(t, atom.CAS(0.0, 1.5), "CAS reported a swap.") - - atom.Store(42.0) - require.Equal(t, float64(42.0), atom.Load(), "Store didn't set the correct value.") - require.Equal(t, float64(42.5), atom.Add(0.5), "Add didn't work.") - require.Equal(t, float64(42.0), atom.Sub(0.5), "Sub didn't work.") - - t.Run("JSON/Marshal", func(t *testing.T) { - atom.Store(42.5) - bytes, err := json.Marshal(atom) - require.NoError(t, err, "json.Marshal errored unexpectedly.") - require.Equal(t, []byte("42.5"), bytes, "json.Marshal encoded the wrong bytes.") - }) - - t.Run("JSON/Unmarshal", func(t *testing.T) { - err := json.Unmarshal([]byte("40.5"), &atom) - require.NoError(t, err, "json.Unmarshal errored unexpectedly.") - require.Equal(t, float64(40.5), atom.Load(), "json.Unmarshal didn't set the correct value.") - }) - - t.Run("JSON/Unmarshal/Error", func(t *testing.T) { - err := json.Unmarshal([]byte("\"40.5\""), &atom) - require.Error(t, err, "json.Unmarshal didn't error as expected.") - assertErrorJSONUnmarshalType(t, err, - "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) - }) - - t.Run("String", func(t *testing.T) { - assert.Equal(t, "42.5", NewFloat64(42.5).String(), - "String() returned an unexpected value.") - }) -} - -func TestDuration(t *testing.T) { - atom := NewDuration(5 * time.Minute) - - require.Equal(t, 5*time.Minute, atom.Load(), "Load didn't work.") - require.Equal(t, 6*time.Minute, atom.Add(time.Minute), "Add didn't work.") - require.Equal(t, 4*time.Minute, atom.Sub(2*time.Minute), "Sub didn't work.") - - require.True(t, atom.CAS(4*time.Minute, time.Minute), "CAS didn't report a swap.") - require.Equal(t, time.Minute, atom.Load(), "CAS didn't set the correct value.") - - require.Equal(t, time.Minute, atom.Swap(2*time.Minute), "Swap didn't return the old value.") - require.Equal(t, 2*time.Minute, atom.Load(), "Swap didn't set the correct value.") - - atom.Store(10 * time.Minute) - require.Equal(t, 10*time.Minute, atom.Load(), "Store didn't set the correct value.") - - t.Run("JSON/Marshal", func(t *testing.T) { - atom.Store(time.Second) - bytes, err := json.Marshal(atom) - require.NoError(t, err, "json.Marshal errored unexpectedly.") - require.Equal(t, []byte("1000000000"), bytes, "json.Marshal encoded the wrong bytes.") - }) - - t.Run("JSON/Unmarshal", func(t *testing.T) { - err := json.Unmarshal([]byte("1000000000"), &atom) - require.NoError(t, err, "json.Unmarshal errored unexpectedly.") - require.Equal(t, time.Second, atom.Load(), "json.Unmarshal didn't set the correct value.") - }) - - t.Run("JSON/Unmarshal/Error", func(t *testing.T) { - err := json.Unmarshal([]byte("\"1000000000\""), &atom) - require.Error(t, err, "json.Unmarshal didn't error as expected.") - assertErrorJSONUnmarshalType(t, err, - "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) - }) - - t.Run("String", func(t *testing.T) { - assert.Equal(t, "42s", NewDuration(42*time.Second).String(), - "String() returned an unexpected value.") - }) -} - -func TestValue(t *testing.T) { - var v Value - assert.Nil(t, v.Load(), "initial Value is not nil") - - v.Store(42) - assert.Equal(t, 42, v.Load()) - - v.Store(84) - assert.Equal(t, 84, v.Load()) - - assert.Panics(t, func() { v.Store("foo") }) -} diff --git a/bool.go b/bool.go new file mode 100644 index 0000000..b22b97d --- /dev/null +++ b/bool.go @@ -0,0 +1,100 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "strconv" + "sync/atomic" +) + +// Bool is an atomic Boolean. +type Bool struct { + nocmp // disallow non-atomic comparison + + v uint32 +} + +// NewBool creates a Bool. +func NewBool(initial bool) *Bool { + return &Bool{v: boolToInt(initial)} +} + +// Load atomically loads the Boolean. +func (b *Bool) Load() bool { + return truthy(atomic.LoadUint32(&b.v)) +} + +// CAS is an atomic compare-and-swap. +func (b *Bool) CAS(old, new bool) bool { + return atomic.CompareAndSwapUint32(&b.v, boolToInt(old), boolToInt(new)) +} + +// Store atomically stores the passed value. +func (b *Bool) Store(new bool) { + atomic.StoreUint32(&b.v, boolToInt(new)) +} + +// Swap sets the given value and returns the previous value. +func (b *Bool) Swap(new bool) bool { + return truthy(atomic.SwapUint32(&b.v, boolToInt(new))) +} + +// Toggle atomically negates the Boolean and returns the previous value. +func (b *Bool) Toggle() bool { + for { + old := b.Load() + if b.CAS(old, !old) { + return old + } + } +} + +func truthy(n uint32) bool { + return n == 1 +} + +func boolToInt(b bool) uint32 { + if b { + return 1 + } + return 0 +} + +// MarshalJSON encodes the wrapped bool into JSON. +func (b *Bool) MarshalJSON() ([]byte, error) { + return json.Marshal(b.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped bool. +func (b *Bool) UnmarshalJSON(t []byte) error { + var v bool + if err := json.Unmarshal(t, &v); err != nil { + return err + } + b.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (b *Bool) String() string { + return strconv.FormatBool(b.Load()) +} diff --git a/bool_test.go b/bool_test.go new file mode 100644 index 0000000..bcba01d --- /dev/null +++ b/bool_test.go @@ -0,0 +1,86 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBool(t *testing.T) { + atom := NewBool(false) + require.False(t, atom.Toggle(), "Expected Toggle to return previous value.") + require.True(t, atom.Toggle(), "Expected Toggle to return previous value.") + require.False(t, atom.Toggle(), "Expected Toggle to return previous value.") + require.True(t, atom.Load(), "Unexpected state after swap.") + + require.True(t, atom.CAS(true, true), "CAS should swap when old matches") + require.True(t, atom.Load(), "CAS should have no effect") + require.True(t, atom.CAS(true, false), "CAS should swap when old matches") + require.False(t, atom.Load(), "CAS should have modified the value") + require.False(t, atom.CAS(true, false), "CAS should fail on old mismatch") + require.False(t, atom.Load(), "CAS should not have modified the value") + + atom.Store(false) + require.False(t, atom.Load(), "Unexpected state after store.") + + prev := atom.Swap(false) + require.False(t, prev, "Expected Swap to return previous value.") + + prev = atom.Swap(true) + require.False(t, prev, "Expected Swap to return previous value.") + + t.Run("JSON/Marshal", func(t *testing.T) { + atom.Store(true) + bytes, err := json.Marshal(atom) + require.NoError(t, err, "json.Marshal errored unexpectedly.") + require.Equal(t, []byte("true"), bytes, "json.Marshal encoded the wrong bytes.") + }) + + t.Run("JSON/Unmarshal", func(t *testing.T) { + err := json.Unmarshal([]byte("false"), &atom) + require.NoError(t, err, "json.Unmarshal errored unexpectedly.") + require.False(t, atom.Load(), "json.Unmarshal didn't set the correct value.") + }) + + t.Run("JSON/Unmarshal/Error", func(t *testing.T) { + err := json.Unmarshal([]byte("42"), &atom) + require.Error(t, err, "json.Unmarshal didn't error as expected.") + assertErrorJSONUnmarshalType(t, err, + "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) + }) + + t.Run("String", func(t *testing.T) { + t.Run("true", func(t *testing.T) { + assert.Equal(t, "true", NewBool(true).String(), + "String() returned an unexpected value.") + }) + + t.Run("false", func(t *testing.T) { + var b Bool + assert.Equal(t, "false", b.String(), + "String() returned an unexpected value.") + }) + }) +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..ae7390e --- /dev/null +++ b/doc.go @@ -0,0 +1,23 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +// Package atomic provides simple wrappers around numerics to enforce atomic +// access. +package atomic diff --git a/duration.go b/duration.go new file mode 100644 index 0000000..92d65f9 --- /dev/null +++ b/duration.go @@ -0,0 +1,89 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "time" +) + +// Duration is an atomic wrapper around time.Duration +// https://godoc.org/time#Duration +type Duration struct { + nocmp // disallow non-atomic comparison + + v Int64 +} + +// NewDuration creates a Duration. +func NewDuration(d time.Duration) *Duration { + return &Duration{v: *NewInt64(int64(d))} +} + +// Load atomically loads the wrapped value. +func (d *Duration) Load() time.Duration { + return time.Duration(d.v.Load()) +} + +// Store atomically stores the passed value. +func (d *Duration) Store(n time.Duration) { + d.v.Store(int64(n)) +} + +// Add atomically adds to the wrapped time.Duration and returns the new value. +func (d *Duration) Add(n time.Duration) time.Duration { + return time.Duration(d.v.Add(int64(n))) +} + +// Sub atomically subtracts from the wrapped time.Duration and returns the new value. +func (d *Duration) Sub(n time.Duration) time.Duration { + return time.Duration(d.v.Sub(int64(n))) +} + +// Swap atomically swaps the wrapped time.Duration and returns the old value. +func (d *Duration) Swap(n time.Duration) time.Duration { + return time.Duration(d.v.Swap(int64(n))) +} + +// CAS is an atomic compare-and-swap. +func (d *Duration) CAS(old, new time.Duration) bool { + return d.v.CAS(int64(old), int64(new)) +} + +// MarshalJSON encodes the wrapped time.Duration into JSON. +func (d *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped time.Duration. +func (d *Duration) UnmarshalJSON(b []byte) error { + var v time.Duration + if err := json.Unmarshal(b, &v); err != nil { + return err + } + d.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (d *Duration) String() string { + return d.Load().String() +} diff --git a/duration_test.go b/duration_test.go new file mode 100644 index 0000000..bc5cea5 --- /dev/null +++ b/duration_test.go @@ -0,0 +1,72 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDuration(t *testing.T) { + atom := NewDuration(5 * time.Minute) + + require.Equal(t, 5*time.Minute, atom.Load(), "Load didn't work.") + require.Equal(t, 6*time.Minute, atom.Add(time.Minute), "Add didn't work.") + require.Equal(t, 4*time.Minute, atom.Sub(2*time.Minute), "Sub didn't work.") + + require.True(t, atom.CAS(4*time.Minute, time.Minute), "CAS didn't report a swap.") + require.Equal(t, time.Minute, atom.Load(), "CAS didn't set the correct value.") + + require.Equal(t, time.Minute, atom.Swap(2*time.Minute), "Swap didn't return the old value.") + require.Equal(t, 2*time.Minute, atom.Load(), "Swap didn't set the correct value.") + + atom.Store(10 * time.Minute) + require.Equal(t, 10*time.Minute, atom.Load(), "Store didn't set the correct value.") + + t.Run("JSON/Marshal", func(t *testing.T) { + atom.Store(time.Second) + bytes, err := json.Marshal(atom) + require.NoError(t, err, "json.Marshal errored unexpectedly.") + require.Equal(t, []byte("1000000000"), bytes, "json.Marshal encoded the wrong bytes.") + }) + + t.Run("JSON/Unmarshal", func(t *testing.T) { + err := json.Unmarshal([]byte("1000000000"), &atom) + require.NoError(t, err, "json.Unmarshal errored unexpectedly.") + require.Equal(t, time.Second, atom.Load(), "json.Unmarshal didn't set the correct value.") + }) + + t.Run("JSON/Unmarshal/Error", func(t *testing.T) { + err := json.Unmarshal([]byte("\"1000000000\""), &atom) + require.Error(t, err, "json.Unmarshal didn't error as expected.") + assertErrorJSONUnmarshalType(t, err, + "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) + }) + + t.Run("String", func(t *testing.T) { + assert.Equal(t, "42s", NewDuration(42*time.Second).String(), + "String() returned an unexpected value.") + }) +} diff --git a/float64.go b/float64.go new file mode 100644 index 0000000..fd9e250 --- /dev/null +++ b/float64.go @@ -0,0 +1,92 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "math" + "strconv" + "sync/atomic" +) + +// Float64 is an atomic wrapper around float64. +type Float64 struct { + nocmp // disallow non-atomic comparison + + v uint64 +} + +// NewFloat64 creates a Float64. +func NewFloat64(f float64) *Float64 { + return &Float64{v: math.Float64bits(f)} +} + +// Load atomically loads the wrapped value. +func (f *Float64) Load() float64 { + return math.Float64frombits(atomic.LoadUint64(&f.v)) +} + +// Store atomically stores the passed value. +func (f *Float64) Store(s float64) { + atomic.StoreUint64(&f.v, math.Float64bits(s)) +} + +// Add atomically adds to the wrapped float64 and returns the new value. +func (f *Float64) Add(s float64) float64 { + for { + old := f.Load() + new := old + s + if f.CAS(old, new) { + return new + } + } +} + +// Sub atomically subtracts from the wrapped float64 and returns the new value. +func (f *Float64) Sub(s float64) float64 { + return f.Add(-s) +} + +// CAS is an atomic compare-and-swap. +func (f *Float64) CAS(old, new float64) bool { + return atomic.CompareAndSwapUint64(&f.v, math.Float64bits(old), math.Float64bits(new)) +} + +// MarshalJSON encodes the wrapped float64 into JSON. +func (f *Float64) MarshalJSON() ([]byte, error) { + return json.Marshal(f.Load()) +} + +// UnmarshalJSON decodes JSON into the wrapped float64. +func (f *Float64) UnmarshalJSON(b []byte) error { + var v float64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + f.Store(v) + return nil +} + +// String encodes the wrapped value as a string. +func (f *Float64) String() string { + // 'g' is the behavior for floats with %v. + return strconv.FormatFloat(f.Load(), 'g', -1, 64) +} diff --git a/float64_test.go b/float64_test.go new file mode 100644 index 0000000..bc04e77 --- /dev/null +++ b/float64_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFloat64(t *testing.T) { + atom := NewFloat64(4.2) + + require.Equal(t, float64(4.2), atom.Load(), "Load didn't work.") + + require.True(t, atom.CAS(4.2, 0.5), "CAS didn't report a swap.") + require.Equal(t, float64(0.5), atom.Load(), "CAS didn't set the correct value.") + require.False(t, atom.CAS(0.0, 1.5), "CAS reported a swap.") + + atom.Store(42.0) + require.Equal(t, float64(42.0), atom.Load(), "Store didn't set the correct value.") + require.Equal(t, float64(42.5), atom.Add(0.5), "Add didn't work.") + require.Equal(t, float64(42.0), atom.Sub(0.5), "Sub didn't work.") + + t.Run("JSON/Marshal", func(t *testing.T) { + atom.Store(42.5) + bytes, err := json.Marshal(atom) + require.NoError(t, err, "json.Marshal errored unexpectedly.") + require.Equal(t, []byte("42.5"), bytes, "json.Marshal encoded the wrong bytes.") + }) + + t.Run("JSON/Unmarshal", func(t *testing.T) { + err := json.Unmarshal([]byte("40.5"), &atom) + require.NoError(t, err, "json.Unmarshal errored unexpectedly.") + require.Equal(t, float64(40.5), atom.Load(), "json.Unmarshal didn't set the correct value.") + }) + + t.Run("JSON/Unmarshal/Error", func(t *testing.T) { + err := json.Unmarshal([]byte("\"40.5\""), &atom) + require.Error(t, err, "json.Unmarshal didn't error as expected.") + assertErrorJSONUnmarshalType(t, err, + "json.Unmarshal failed with unexpected error %v, want UnmarshalTypeError.", err) + }) + + t.Run("String", func(t *testing.T) { + assert.Equal(t, "42.5", NewFloat64(42.5).String(), + "String() returned an unexpected value.") + }) +} diff --git a/value.go b/value.go new file mode 100644 index 0000000..5c32903 --- /dev/null +++ b/value.go @@ -0,0 +1,30 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import "sync/atomic" + +// Value shadows the type of the same name from sync/atomic +// https://godoc.org/sync/atomic#Value +type Value struct { + nocmp // disallow non-atomic comparison + atomic.Value +} diff --git a/value_test.go b/value_test.go new file mode 100644 index 0000000..bb9f301 --- /dev/null +++ b/value_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package atomic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValue(t *testing.T) { + var v Value + assert.Nil(t, v.Load(), "initial Value is not nil") + + v.Store(42) + assert.Equal(t, 42, v.Load()) + + v.Store(84) + assert.Equal(t, 84, v.Load()) + + assert.Panics(t, func() { v.Store("foo") }) +}