diff --git a/Makefile b/Makefile index 7203311..e3b3c2c 100644 --- a/Makefile +++ b/Makefile @@ -3,14 +3,14 @@ export GOBIN ?= $(shell pwd)/bin GOLINT = $(GOBIN)/golint GEN_ATOMICINT = $(GOBIN)/gen-atomicint -GEN_VALUEWRAPPER = $(GOBIN)/gen-valuewrapper +GEN_ATOMICWRAPPER = $(GOBIN)/gen-atomicwrapper GO_FILES ?= $(shell find . '(' -path .git -o -path vendor ')' -prune -o -name '*.go' -print) # Also update ignore section in .codecov.yml. COVER_IGNORE_PKGS = \ go.uber.org/atomic/internal/gen-atomicint \ - go.uber.org/atomic/internal/gen-valuewrapper + go.uber.org/atomic/internal/gen-atomicwrapper .PHONY: build build: @@ -29,8 +29,8 @@ gofmt: $(GOLINT): go install golang.org/x/lint/golint -$(GEN_VALUEWRAPPER): $(wildcard ./internal/gen-valuewrapper/*) - go build -o $@ ./internal/gen-valuewrapper +$(GEN_ATOMICWRAPPER): $(wildcard ./internal/gen-atomicwrapper/*) + go build -o $@ ./internal/gen-atomicwrapper $(GEN_ATOMICINT): $(wildcard ./internal/gen-atomicint/*) go build -o $@ ./internal/gen-atomicint @@ -54,7 +54,7 @@ cover: go tool cover -html=cover.out -o cover.html .PHONY: generate -generate: $(GEN_ATOMICINT) $(GEN_VALUEWRAPPER) +generate: $(GEN_ATOMICINT) $(GEN_ATOMICWRAPPER) go generate ./... .PHONY: generatenodirty 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..1bf3fa0 --- /dev/null +++ b/bool.go @@ -0,0 +1,81 @@ +// @generated Code generated by gen-atomicwrapper. + +// 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" +) + +// Bool is an atomic type-safe wrapper for bool values. +type Bool struct { + nocmp // disallow non-atomic comparison + + v Uint32 +} + +var _zeroBool bool + +// NewBool creates a new Bool. +func NewBool(v bool) *Bool { + x := &Bool{} + if v != _zeroBool { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped bool. +func (x *Bool) Load() bool { + return truthy(x.v.Load()) +} + +// Store atomically stores the passed bool. +func (x *Bool) Store(v bool) { + x.v.Store(boolToInt(v)) +} + +// CAS is an atomic compare-and-swap for bool values. +func (x *Bool) CAS(o, n bool) bool { + return x.v.CAS(boolToInt(o), boolToInt(n)) +} + +// Swap atomically stores the given bool and returns the old +// value. +func (x *Bool) Swap(o bool) bool { + return truthy(x.v.Swap(boolToInt(o))) +} + +// MarshalJSON encodes the wrapped bool into JSON. +func (x *Bool) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a bool from JSON. +func (x *Bool) UnmarshalJSON(b []byte) error { + var v bool + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/bool_ext.go b/bool_ext.go new file mode 100644 index 0000000..c7bf7a8 --- /dev/null +++ b/bool_ext.go @@ -0,0 +1,53 @@ +// 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 ( + "strconv" +) + +//go:generate bin/gen-atomicwrapper -name=Bool -type=bool -wrapped=Uint32 -pack=boolToInt -unpack=truthy -cas -swap -json -file=bool.go + +func truthy(n uint32) bool { + return n == 1 +} + +func boolToInt(b bool) uint32 { + if b { + return 1 + } + return 0 +} + +// 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 + } + } +} + +// 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..30ed6aa --- /dev/null +++ b/duration.go @@ -0,0 +1,82 @@ +// @generated Code generated by gen-atomicwrapper. + +// 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 type-safe wrapper for time.Duration values. +type Duration struct { + nocmp // disallow non-atomic comparison + + v Int64 +} + +var _zeroDuration time.Duration + +// NewDuration creates a new Duration. +func NewDuration(v time.Duration) *Duration { + x := &Duration{} + if v != _zeroDuration { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped time.Duration. +func (x *Duration) Load() time.Duration { + return time.Duration(x.v.Load()) +} + +// Store atomically stores the passed time.Duration. +func (x *Duration) Store(v time.Duration) { + x.v.Store(int64(v)) +} + +// CAS is an atomic compare-and-swap for time.Duration values. +func (x *Duration) CAS(o, n time.Duration) bool { + return x.v.CAS(int64(o), int64(n)) +} + +// Swap atomically stores the given time.Duration and returns the old +// value. +func (x *Duration) Swap(o time.Duration) time.Duration { + return time.Duration(x.v.Swap(int64(o))) +} + +// MarshalJSON encodes the wrapped time.Duration into JSON. +func (x *Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a time.Duration from JSON. +func (x *Duration) UnmarshalJSON(b []byte) error { + var v time.Duration + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/duration_ext.go b/duration_ext.go new file mode 100644 index 0000000..6273b66 --- /dev/null +++ b/duration_ext.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 "time" + +//go:generate bin/gen-atomicwrapper -name=Duration -type=time.Duration -wrapped=Int64 -pack=int64 -unpack=time.Duration -cas -swap -json -imports time -file=duration.go + +// 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))) +} + +// 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/error.go b/error.go index 8657849..cedbc91 100644 --- a/error.go +++ b/error.go @@ -1,4 +1,4 @@ -// @generated Code generated by gen-valuewrapper. +// @generated Code generated by gen-atomicwrapper. // Copyright (c) 2020 Uber Technologies, Inc. // @@ -23,22 +23,18 @@ package atomic // Error is an atomic type-safe wrapper for error values. -type Error struct{ v Value } +type Error struct { + nocmp // disallow non-atomic comparison -type storedError struct{ Value error } - -func wrapError(v error) storedError { - return storedError{v} + v Value } -func unwrapError(v storedError) error { - return v.Value -} +var _zeroError error // NewError creates a new Error. func NewError(v error) *Error { x := &Error{} - if v != nil { + if v != _zeroError { x.Store(v) } return x @@ -46,16 +42,10 @@ func NewError(v error) *Error { // Load atomically loads the wrapped error. func (x *Error) Load() error { - v := x.v.Load() - if v == nil { - return nil - } - return unwrapError(v.(storedError)) + return unpackError(x.v.Load()) } // Store atomically stores the passed error. -// -// NOTE: This will cause an allocation. func (x *Error) Store(v error) { - x.v.Store(wrapError(v)) + x.v.Store(packError(v)) } diff --git a/error_ext.go b/error_ext.go new file mode 100644 index 0000000..ffe0be2 --- /dev/null +++ b/error_ext.go @@ -0,0 +1,39 @@ +// 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 + +// atomic.Value panics on nil inputs, or if the underlying type changes. +// Stabilize by always storing a custom struct that we control. + +//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -file=error.go + +type packedError struct{ Value error } + +func packError(v error) interface{} { + return packedError{v} +} + +func unpackError(v interface{}) error { + if err, ok := v.(packedError); ok { + return err.Value + } + return nil +} diff --git a/float64.go b/float64.go new file mode 100644 index 0000000..fb33e90 --- /dev/null +++ b/float64.go @@ -0,0 +1,76 @@ +// @generated Code generated by gen-atomicwrapper. + +// 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" +) + +// Float64 is an atomic type-safe wrapper for float64 values. +type Float64 struct { + nocmp // disallow non-atomic comparison + + v Uint64 +} + +var _zeroFloat64 float64 + +// NewFloat64 creates a new Float64. +func NewFloat64(v float64) *Float64 { + x := &Float64{} + if v != _zeroFloat64 { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped float64. +func (x *Float64) Load() float64 { + return math.Float64frombits(x.v.Load()) +} + +// Store atomically stores the passed float64. +func (x *Float64) Store(v float64) { + x.v.Store(math.Float64bits(v)) +} + +// CAS is an atomic compare-and-swap for float64 values. +func (x *Float64) CAS(o, n float64) bool { + return x.v.CAS(math.Float64bits(o), math.Float64bits(n)) +} + +// MarshalJSON encodes the wrapped float64 into JSON. +func (x *Float64) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) +} + +// UnmarshalJSON decodes a float64 from JSON. +func (x *Float64) UnmarshalJSON(b []byte) error { + var v float64 + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil +} diff --git a/float64_ext.go b/float64_ext.go new file mode 100644 index 0000000..927b1ad --- /dev/null +++ b/float64_ext.go @@ -0,0 +1,47 @@ +// 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 "strconv" + +//go:generate bin/gen-atomicwrapper -name=Float64 -type=float64 -wrapped=Uint64 -pack=math.Float64bits -unpack=math.Float64frombits -cas -json -imports math -file=float64.go + +// 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) +} + +// 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/gen.go b/gen.go index 03c8ac9..50d6b24 100644 --- a/gen.go +++ b/gen.go @@ -24,6 +24,3 @@ package atomic //go:generate bin/gen-atomicint -name=Int64 -wrapped=int64 -file=int64.go //go:generate bin/gen-atomicint -name=Uint32 -wrapped=uint32 -unsigned -file=uint32.go //go:generate bin/gen-atomicint -name=Uint64 -wrapped=uint64 -unsigned -file=uint64.go - -//go:generate bin/gen-valuewrapper -name=String -type=string -zero="" -file=string.go -//go:generate bin/gen-valuewrapper -name=Error -type=error -zero=nil -file=error.go diff --git a/internal/gen-atomicwrapper/main.go b/internal/gen-atomicwrapper/main.go new file mode 100644 index 0000000..0bb180e --- /dev/null +++ b/internal/gen-atomicwrapper/main.go @@ -0,0 +1,300 @@ +// 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. + +// gen-atomicwrapper generates wrapper types around other atomic types. +// +// It supports plugging in functions which convert the value inside the atomic +// type to the user-facing value. For example, +// +// Given, atomic.Value and the functions, +// +// func packString(string) interface{} +// func unpackString(interface{}) string +// +// We can run the following command: +// +// gen-atomicwrapper -name String -wrapped Value \ +// -type string -pack fromString -unpack tostring +// +// This wil generate approximately, +// +// type String struct{ v Value } +// +// func (s *String) Load() string { +// return unpackString(v.Load()) +// } +// +// func (s *String) Store(s string) { +// return s.v.Store(packString(s)) +// } +// +// The packing/unpacking logic allows the stored value to be different from +// the user-facing value. +// +// Without -pack and -unpack, the output will be cast to the target type, +// defaulting to the zero value. +package main + +import ( + "bytes" + "errors" + "flag" + "fmt" + "go/format" + "io" + "log" + "os" + "sort" + "strings" + "text/template" +) + +func main() { + log.SetFlags(0) + if err := run(os.Args[1:]); err != nil { + log.Fatalf("%+v", err) + } +} + +type stringList []string + +func (sl *stringList) String() string { + return strings.Join(*sl, ",") +} + +func (sl *stringList) Set(s string) error { + for _, i := range strings.Split(s, ",") { + *sl = append(*sl, strings.TrimSpace(i)) + } + return nil +} + +func run(args []string) error { + var opts struct { + Name string + Wrapped string + Type string + + Imports stringList + Pack, Unpack string + + CAS bool + Swap bool + JSON bool + + File string + } + + flag := flag.NewFlagSet("gen-atomicwrapper", flag.ContinueOnError) + + // Required flags + flag.StringVar(&opts.Name, "name", "", + "name of the generated type (e.g. Duration)") + flag.StringVar(&opts.Wrapped, "wrapped", "", + "name of the wrapped atomic (e.g. Int64)") + flag.StringVar(&opts.Type, "type", "", + "name of the type exposed by the atomic (e.g. time.Duration)") + + // Optional flags + flag.Var(&opts.Imports, "imports", + "comma separated list of imports to add") + flag.StringVar(&opts.Pack, "pack", "", + "function to transform values with before storage") + flag.StringVar(&opts.Unpack, "unpack", "", + "function to reverse packing on loading") + flag.StringVar(&opts.File, "file", "", + "output file path (default: stdout)") + + // Switches for individual methods. Underlying atomics must support + // these. + flag.BoolVar(&opts.CAS, "cas", false, + "generate a `CAS(old, new) bool` method; requires -pack") + flag.BoolVar(&opts.Swap, "swap", false, + "generate a `Swap(new) old` method; requires -pack and -unpack") + flag.BoolVar(&opts.JSON, "json", false, + "generate `MarshalJSON/UnmarshJSON` methods") + + if err := flag.Parse(args); err != nil { + return err + } + + if len(opts.Name) == 0 || len(opts.Wrapped) == 0 || len(opts.Type) == 0 { + return errors.New("flags -name, -wrapped, and -type are required") + } + + if (len(opts.Pack) == 0) != (len(opts.Unpack) == 0) { + return errors.New("either both, or neither of -pack and -unpack must be specified") + } + + if opts.CAS && len(opts.Pack) == 0 { + return errors.New("flag -cas requires -pack") + } + + if opts.Swap && len(opts.Pack) == 0 { + return errors.New("flag -swap requires -pack and -unpack") + } + + var w io.Writer = os.Stdout + if file := opts.File; len(file) > 0 { + f, err := os.Create(file) + if err != nil { + return fmt.Errorf("create %q: %v", file, err) + } + defer f.Close() + + w = f + } + + // Import encoding/json if needed. + if opts.JSON { + found := false + for _, imp := range opts.Imports { + if imp == "encoding/json" { + found = true + break + } + } + + if !found { + opts.Imports = append(opts.Imports, "encoding/json") + } + } + + sort.Strings([]string(opts.Imports)) + + var buff bytes.Buffer + if err := _tmpl.Execute(&buff, opts); err != nil { + return fmt.Errorf("render template: %v", err) + } + + bs, err := format.Source(buff.Bytes()) + if err != nil { + return fmt.Errorf("reformat source: %v", err) + } + + _, err = w.Write(bs) + return err +} + +var _tmpl = template.Must(template.New("int.go").Parse(`// @generated Code generated by gen-atomicwrapper. + +// 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 + +{{ with .Imports }} +import ( + {{ range . -}} + {{ printf "%q" . }} + {{ end }} +) +{{ end }} + +// {{ .Name }} is an atomic type-safe wrapper for {{ .Type }} values. +type {{ .Name }} struct{ + nocmp // disallow non-atomic comparison + + v {{ .Wrapped }} +} + +var _zero{{ .Name }} {{ .Type }} + + +// New{{ .Name }} creates a new {{ .Name }}. +func New{{ .Name}}(v {{ .Type }}) *{{ .Name }} { + x := &{{ .Name }}{} + if v != _zero{{ .Name }} { + x.Store(v) + } + return x +} + +// Load atomically loads the wrapped {{ .Type }}. +func (x *{{ .Name }}) Load() {{ .Type }} { + {{ if .Unpack -}} + return {{ .Unpack }}(x.v.Load()) + {{- else -}} + if v := x.v.Load(); v != nil { + return v.({{ .Type }}) + } + return _zero{{ .Name }} + {{- end }} +} + +// Store atomically stores the passed {{ .Type }}. +func (x *{{ .Name }}) Store(v {{ .Type }}) { + {{ if .Pack -}} + x.v.Store({{ .Pack }}(v)) + {{- else -}} + x.v.Store(v) + {{- end }} +} + +{{ if .CAS -}} + // CAS is an atomic compare-and-swap for {{ .Type }} values. + func (x *{{ .Name }}) CAS(o, n {{ .Type }}) bool { + return x.v.CAS({{ .Pack }}(o), {{ .Pack }}(n)) + } +{{- end }} + +{{ if .Swap -}} + // Swap atomically stores the given {{ .Type }} and returns the old + // value. + func (x *{{ .Name }}) Swap(o {{ .Type }}) {{ .Type }} { + return {{ .Unpack }}(x.v.Swap({{ .Pack }}(o))) + } +{{- end }} + +{{ if .JSON -}} + // MarshalJSON encodes the wrapped {{ .Type }} into JSON. + func (x *{{ .Name }}) MarshalJSON() ([]byte, error) { + return json.Marshal(x.Load()) + } + + // UnmarshalJSON decodes a {{ .Type }} from JSON. + func (x *{{ .Name }}) UnmarshalJSON(b []byte) error { + var v {{ .Type }} + if err := json.Unmarshal(b, &v); err != nil { + return err + } + x.Store(v) + return nil + } +{{- end }} + +`)) diff --git a/internal/gen-valuewrapper/main.go b/internal/gen-valuewrapper/main.go deleted file mode 100644 index 520701e..0000000 --- a/internal/gen-valuewrapper/main.go +++ /dev/null @@ -1,173 +0,0 @@ -// 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 main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "go/format" - "io" - "log" - "os" - "text/template" -) - -func main() { - log.SetFlags(0) - if err := run(os.Args[1:]); err != nil { - log.Fatalf("%+v", err) - } -} - -func run(args []string) error { - var opts struct { - Name string - Type string - Zero string - File string - } - - flag := flag.NewFlagSet("gen-atomicint", flag.ContinueOnError) - - flag.StringVar(&opts.Name, "name", "", "name of the generated type (e.g. Int32)") - flag.StringVar(&opts.Type, "type", "", "name of the wrapped type (e.g. int32)") - flag.StringVar(&opts.Zero, "zero", "", "zero value of the wrapped type (e.g. nil)") - flag.StringVar(&opts.File, "file", "", "output file path (default: stdout)") - - if err := flag.Parse(args); err != nil { - return err - } - - if len(opts.Name) == 0 || len(opts.Type) == 0 || len(opts.Zero) == 0 { - return errors.New("flags -name, -type, and -zero are required") - } - - var w io.Writer = os.Stdout - if file := opts.File; len(file) > 0 { - f, err := os.Create(file) - if err != nil { - return fmt.Errorf("create %q: %v", file, err) - } - defer f.Close() - - w = f - } - - data := struct { - Name string - Type string - Zero string - Nillable bool - }{ - Name: opts.Name, - Type: opts.Type, - Zero: opts.Zero, - Nillable: opts.Zero == "nil", - } - - var buff bytes.Buffer - if err := _tmpl.Execute(&buff, data); err != nil { - return fmt.Errorf("render template: %v", err) - } - - bs, err := format.Source(buff.Bytes()) - if err != nil { - return fmt.Errorf("reformat source: %v", err) - } - - _, err = w.Write(bs) - return err -} - -var _tmpl = template.Must(template.New("int.go").Parse(`// @generated Code generated by gen-valuewrapper. - -// 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 - -// {{ .Name }} is an atomic type-safe wrapper for {{ .Type }} values. -type {{ .Name }} struct{ v Value } - -{{/* atomic.Value panics for nil values. Generate a wrapper for these types. */}} -{{ $stored := .Type }} -{{ $wrap := .Type }} -{{ $unwrap := .Type }} -{{ if .Nillable -}} - {{ $stored = printf "stored%s" .Name }} - {{ $wrap = printf "wrap%s" .Name }} - {{ $unwrap = printf "unwrap%s" .Name }} - - type {{ $stored }} struct{ Value {{ .Type }} } - - func {{ $wrap }}(v {{ .Type }}) {{ $stored }} { - return {{ $stored }}{v} - } - - func {{ $unwrap }}(v {{ $stored }}) {{ .Type }} { - return v.Value - } -{{- end }} - -// New{{ .Name }} creates a new {{ .Name }}. -func New{{ .Name }}(v {{ .Type }}) *{{ .Name }} { - x := &{{ .Name }}{} - if v != {{ .Zero }} { - x.Store(v) - } - return x -} - -// Load atomically loads the wrapped {{ .Type }}. -func (x *{{ .Name }}) Load() {{ .Type }} { - v := x.v.Load() - if v == nil { - return {{ .Zero }} - } - return {{ $unwrap }}(v.({{ $stored }})) -} - -// Store atomically stores the passed {{ .Type }}. -// -// NOTE: This will cause an allocation. -func (x *{{ .Name }}) Store(v {{ .Type }}) { - x.v.Store({{ $wrap }}(v)) -} -`)) diff --git a/string.go b/string.go index 8829c48..8e3ac35 100644 --- a/string.go +++ b/string.go @@ -1,4 +1,4 @@ -// @generated Code generated by gen-valuewrapper. +// @generated Code generated by gen-atomicwrapper. // Copyright (c) 2020 Uber Technologies, Inc. // @@ -23,12 +23,18 @@ package atomic // String is an atomic type-safe wrapper for string values. -type String struct{ v Value } +type String struct { + nocmp // disallow non-atomic comparison + + v Value +} + +var _zeroString string // NewString creates a new String. func NewString(v string) *String { x := &String{} - if v != "" { + if v != _zeroString { x.Store(v) } return x @@ -36,16 +42,13 @@ func NewString(v string) *String { // Load atomically loads the wrapped string. func (x *String) Load() string { - v := x.v.Load() - if v == nil { - return "" + if v := x.v.Load(); v != nil { + return v.(string) } - return string(v.(string)) + return _zeroString } // Store atomically stores the passed string. -// -// NOTE: This will cause an allocation. func (x *String) Store(v string) { - x.v.Store(string(v)) + x.v.Store(v) } diff --git a/string_ext.go b/string_ext.go index a346358..3a95582 100644 --- a/string_ext.go +++ b/string_ext.go @@ -20,6 +20,8 @@ package atomic +//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped=Value -file=string.go + // String returns the wrapped value. func (s *String) String() string { return s.Load() 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") }) +}