diff --git a/pkg/syncutil/once.go b/pkg/syncutil/once.go new file mode 100644 index 000000000..1123f0923 --- /dev/null +++ b/pkg/syncutil/once.go @@ -0,0 +1,60 @@ +/* +Copyright 2014 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package syncutil + +import ( + "sync" + "sync/atomic" +) + +// A Once will perform a successful action exactly once. +// +// Unlike a sync.Once, this Once's func returns an error +// and is re-armed on failure. +type Once struct { + m sync.Mutex + done uint32 +} + +// Do calls the function f if and only if Do has not been invoked +// without error for this instance of Once. In other words, given +// var once Once +// if once.Do(f) is called multiple times, only the first call will +// invoke f, even if f has a different value in each invocation unless +// f returns an error. A new instance of Once is required for each +// function to execute. +// +// Do is intended for initialization that must be run exactly once. Since f +// is niladic, it may be necessary to use a function literal to capture the +// arguments to a function to be invoked by Do: +// err := config.once.Do(func() error { return config.init(filename) }) +func (o *Once) Do(f func() error) error { + if atomic.LoadUint32(&o.done) == 1 { + return nil + } + // Slow-path. + o.m.Lock() + defer o.m.Unlock() + var err error + if o.done == 0 { + err = f() + if err == nil { + atomic.StoreUint32(&o.done, 1) + } + } + return err +} diff --git a/pkg/syncutil/once_test.go b/pkg/syncutil/once_test.go new file mode 100644 index 000000000..e321d5095 --- /dev/null +++ b/pkg/syncutil/once_test.go @@ -0,0 +1,57 @@ +package syncutil + +import ( + "errors" + "testing" +) + +func TestOnce(t *testing.T) { + timesRan := 0 + f := func() error { + timesRan++ + return nil + } + + once := Once{} + grp := Group{} + + for i := 0; i < 10; i++ { + grp.Go(func() error { return once.Do(f) }) + } + + if grp.Err() != nil { + t.Errorf("Expected no errors, got %v", grp.Err()) + } + + if timesRan != 1 { + t.Errorf("Expected to run one time, ran %d", timesRan) + } +} + +// TestOnceErroring verifies we retry on every error, but stop after +// the first success. +func TestOnceErroring(t *testing.T) { + timesRan := 0 + f := func() error { + timesRan++ + if timesRan < 3 { + return errors.New("retry") + } + return nil + } + + once := Once{} + grp := Group{} + + for i := 0; i < 10; i++ { + grp.Go(func() error { return once.Do(f) }) + } + + if len(grp.Errs()) != 2 { + t.Errorf("Expected two errors, got %d", len(grp.Errs())) + } + + if timesRan != 3 { + t.Errorf("Expected to run two times, ran %d", timesRan) + } +}