-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add multicloser implementation (#136)
- Loading branch information
Showing
3 changed files
with
251 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
// Copyright 2023 The Authors (see AUTHORS file) | ||
// | ||
// 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 multicloser provides a convenient way to join multiple "close" | ||
// functions together so they can be called together. This is especially useful | ||
// to group multiple cleanup function calls and return it as a single "closer" | ||
// to be called later. | ||
package multicloser | ||
|
||
import ( | ||
"errors" | ||
) | ||
|
||
// Func is the type signature for a closing function. It accepts a function that | ||
// returns an error or a void function. | ||
type Func interface { | ||
func() error | func() | ||
} | ||
|
||
// Closer maintains the ordered list of closing functions. Functions will be run | ||
// in the order in which they were inserted. | ||
// | ||
// It is not safe to use concurrently without locking. | ||
type Closer struct { | ||
fns []func() error | ||
} | ||
|
||
// Append adds the given closer functions. It handles void and error signatures. | ||
// Other signatures should use an anonymous function to match an expected | ||
// signature. | ||
func Append[T Func](c *Closer, fns ...T) *Closer { | ||
if c == nil { | ||
c = new(Closer) | ||
} | ||
|
||
for _, fn := range fns { | ||
if fn == nil { | ||
continue | ||
} | ||
|
||
switch typ := any(fn).(type) { | ||
case func() error: | ||
c.fns = append(c.fns, typ) | ||
case func(): | ||
c.fns = append(c.fns, func() error { | ||
typ() | ||
return nil | ||
}) | ||
default: | ||
panic("impossible") | ||
} | ||
} | ||
|
||
return c | ||
} | ||
|
||
// Close runs all closer functions. All closers are guaranteed to run, even if | ||
// they panic. After all closers run, panics will propagate up the stack. | ||
// | ||
// [Close] also panics if it is called on an already-closed Closer. | ||
func (c *Closer) Close() (err error) { | ||
if c == nil { | ||
return | ||
} | ||
|
||
for i := len(c.fns) - 1; i >= 0; i-- { | ||
fn := c.fns[i] | ||
if fn != nil { | ||
// We abuse defer's automatic panic recovery here a bit.. | ||
defer func() { | ||
err = errors.Join(err, fn()) | ||
}() | ||
} | ||
} | ||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// Copyright 2023 The Authors (see AUTHORS file) | ||
// | ||
// 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 multicloser_test | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
|
||
"github.com/abcxyz/pkg/multicloser" | ||
) | ||
|
||
func setup() (*multicloser.Closer, error) { | ||
var closer *multicloser.Closer | ||
|
||
client1, err := newClient() | ||
if err != nil { | ||
return closer, fmt.Errorf("failed to create client1: %w", err) | ||
} | ||
closer = multicloser.Append(closer, client1.Close) | ||
|
||
client2, err := newClient() | ||
if err != nil { | ||
return closer, fmt.Errorf("failed to create client2: %w", err) | ||
} | ||
closer = multicloser.Append(closer, client2.Close) | ||
|
||
return closer, nil | ||
} | ||
|
||
// client is just a stub to demonstrate something that needs to be closed. | ||
type client struct{} | ||
|
||
func (c *client) Close() error { | ||
return nil | ||
} | ||
|
||
func newClient() (*client, error) { | ||
return &client{}, nil | ||
} | ||
|
||
func Example() { | ||
closer, err := setup() | ||
defer func() { | ||
if err := closer.Close(); err != nil { | ||
log.Printf("failed to close: %s\n", err) | ||
} | ||
}() | ||
if err != nil { | ||
// handle err | ||
} | ||
|
||
// Output: | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
// Copyright 2023 The Authors (see AUTHORS file) | ||
// | ||
// 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 multicloser | ||
|
||
import ( | ||
"fmt" | ||
"testing" | ||
|
||
"github.com/abcxyz/pkg/testutil" | ||
) | ||
|
||
func TestAppend(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("nil_closer", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
c := Append(nil, func() {}) | ||
if c == nil { | ||
t.Error("expected not nil") | ||
} | ||
}) | ||
|
||
t.Run("nil_func", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
var c *Closer | ||
c = Append(c, (func())(nil)) | ||
if got, want := len(c.fns), 0; got != want { | ||
t.Errorf("expected %d to be %d: %v", got, want, c.fns) | ||
} | ||
}) | ||
|
||
t.Run("variadic", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
var c *Closer | ||
c = Append(c, func() {}, func() {}) | ||
c = Append(c, func() error { return nil }, func() error { return nil }) | ||
if got, want := len(c.fns), 4; got != want { | ||
t.Errorf("expected %d to be %d: %v", got, want, c.fns) | ||
} | ||
}) | ||
} | ||
|
||
func TestClose(t *testing.T) { | ||
t.Parallel() | ||
|
||
t.Run("nil_closer", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// This test is mostly checking to ensure we don't panic. | ||
var c *Closer | ||
if err := c.Close(); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
|
||
t.Run("nil_func", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
// We have to write directly to the slice to bypass the validation in Append. | ||
c := &Closer{} | ||
c.fns = append(c.fns, nil, nil) | ||
if err := c.Close(); err != nil { | ||
t.Fatal(err) | ||
} | ||
}) | ||
|
||
t.Run("ordered", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
var c *Closer | ||
for i := 0; i < 5; i++ { | ||
i := i | ||
c = Append(c, func() error { | ||
return fmt.Errorf("%d", i) | ||
}) | ||
} | ||
|
||
got := c.Close() | ||
want := "0\n1\n2\n3\n4" | ||
if diff := testutil.DiffErrString(got, want); diff != "" { | ||
t.Errorf(diff) | ||
} | ||
}) | ||
} |