Skip to content

Commit 84da38d

Browse files
authored
Add multicloser implementation (#136)
1 parent 0d3628b commit 84da38d

File tree

3 files changed

+251
-0
lines changed

3 files changed

+251
-0
lines changed

multicloser/multicloser.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Copyright 2023 The Authors (see AUTHORS file)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package multicloser provides a convenient way to join multiple "close"
16+
// functions together so they can be called together. This is especially useful
17+
// to group multiple cleanup function calls and return it as a single "closer"
18+
// to be called later.
19+
package multicloser
20+
21+
import (
22+
"errors"
23+
)
24+
25+
// Func is the type signature for a closing function. It accepts a function that
26+
// returns an error or a void function.
27+
type Func interface {
28+
func() error | func()
29+
}
30+
31+
// Closer maintains the ordered list of closing functions. Functions will be run
32+
// in the order in which they were inserted.
33+
//
34+
// It is not safe to use concurrently without locking.
35+
type Closer struct {
36+
fns []func() error
37+
}
38+
39+
// Append adds the given closer functions. It handles void and error signatures.
40+
// Other signatures should use an anonymous function to match an expected
41+
// signature.
42+
func Append[T Func](c *Closer, fns ...T) *Closer {
43+
if c == nil {
44+
c = new(Closer)
45+
}
46+
47+
for _, fn := range fns {
48+
if fn == nil {
49+
continue
50+
}
51+
52+
switch typ := any(fn).(type) {
53+
case func() error:
54+
c.fns = append(c.fns, typ)
55+
case func():
56+
c.fns = append(c.fns, func() error {
57+
typ()
58+
return nil
59+
})
60+
default:
61+
panic("impossible")
62+
}
63+
}
64+
65+
return c
66+
}
67+
68+
// Close runs all closer functions. All closers are guaranteed to run, even if
69+
// they panic. After all closers run, panics will propagate up the stack.
70+
//
71+
// [Close] also panics if it is called on an already-closed Closer.
72+
func (c *Closer) Close() (err error) {
73+
if c == nil {
74+
return
75+
}
76+
77+
for i := len(c.fns) - 1; i >= 0; i-- {
78+
fn := c.fns[i]
79+
if fn != nil {
80+
// We abuse defer's automatic panic recovery here a bit..
81+
defer func() {
82+
err = errors.Join(err, fn())
83+
}()
84+
}
85+
}
86+
return
87+
}

multicloser/multicloser_doc_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2023 The Authors (see AUTHORS file)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package multicloser_test
16+
17+
import (
18+
"fmt"
19+
"log"
20+
21+
"github.com/abcxyz/pkg/multicloser"
22+
)
23+
24+
func setup() (*multicloser.Closer, error) {
25+
var closer *multicloser.Closer
26+
27+
client1, err := newClient()
28+
if err != nil {
29+
return closer, fmt.Errorf("failed to create client1: %w", err)
30+
}
31+
closer = multicloser.Append(closer, client1.Close)
32+
33+
client2, err := newClient()
34+
if err != nil {
35+
return closer, fmt.Errorf("failed to create client2: %w", err)
36+
}
37+
closer = multicloser.Append(closer, client2.Close)
38+
39+
return closer, nil
40+
}
41+
42+
// client is just a stub to demonstrate something that needs to be closed.
43+
type client struct{}
44+
45+
func (c *client) Close() error {
46+
return nil
47+
}
48+
49+
func newClient() (*client, error) {
50+
return &client{}, nil
51+
}
52+
53+
func Example() {
54+
closer, err := setup()
55+
defer func() {
56+
if err := closer.Close(); err != nil {
57+
log.Printf("failed to close: %s\n", err)
58+
}
59+
}()
60+
if err != nil {
61+
// handle err
62+
}
63+
64+
// Output:
65+
}

multicloser/multicloser_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2023 The Authors (see AUTHORS file)
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package multicloser
16+
17+
import (
18+
"fmt"
19+
"testing"
20+
21+
"github.com/abcxyz/pkg/testutil"
22+
)
23+
24+
func TestAppend(t *testing.T) {
25+
t.Parallel()
26+
27+
t.Run("nil_closer", func(t *testing.T) {
28+
t.Parallel()
29+
30+
c := Append(nil, func() {})
31+
if c == nil {
32+
t.Error("expected not nil")
33+
}
34+
})
35+
36+
t.Run("nil_func", func(t *testing.T) {
37+
t.Parallel()
38+
39+
var c *Closer
40+
c = Append(c, (func())(nil))
41+
if got, want := len(c.fns), 0; got != want {
42+
t.Errorf("expected %d to be %d: %v", got, want, c.fns)
43+
}
44+
})
45+
46+
t.Run("variadic", func(t *testing.T) {
47+
t.Parallel()
48+
49+
var c *Closer
50+
c = Append(c, func() {}, func() {})
51+
c = Append(c, func() error { return nil }, func() error { return nil })
52+
if got, want := len(c.fns), 4; got != want {
53+
t.Errorf("expected %d to be %d: %v", got, want, c.fns)
54+
}
55+
})
56+
}
57+
58+
func TestClose(t *testing.T) {
59+
t.Parallel()
60+
61+
t.Run("nil_closer", func(t *testing.T) {
62+
t.Parallel()
63+
64+
// This test is mostly checking to ensure we don't panic.
65+
var c *Closer
66+
if err := c.Close(); err != nil {
67+
t.Fatal(err)
68+
}
69+
})
70+
71+
t.Run("nil_func", func(t *testing.T) {
72+
t.Parallel()
73+
74+
// We have to write directly to the slice to bypass the validation in Append.
75+
c := &Closer{}
76+
c.fns = append(c.fns, nil, nil)
77+
if err := c.Close(); err != nil {
78+
t.Fatal(err)
79+
}
80+
})
81+
82+
t.Run("ordered", func(t *testing.T) {
83+
t.Parallel()
84+
85+
var c *Closer
86+
for i := 0; i < 5; i++ {
87+
i := i
88+
c = Append(c, func() error {
89+
return fmt.Errorf("%d", i)
90+
})
91+
}
92+
93+
got := c.Close()
94+
want := "0\n1\n2\n3\n4"
95+
if diff := testutil.DiffErrString(got, want); diff != "" {
96+
t.Errorf(diff)
97+
}
98+
})
99+
}

0 commit comments

Comments
 (0)