Skip to content

Commit

Permalink
Add multicloser implementation (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
sethvargo authored Jun 11, 2023
1 parent 0d3628b commit 84da38d
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 0 deletions.
87 changes: 87 additions & 0 deletions multicloser/multicloser.go
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
}
65 changes: 65 additions & 0 deletions multicloser/multicloser_doc_test.go
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:
}
99 changes: 99 additions & 0 deletions multicloser/multicloser_test.go
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)
}
})
}

0 comments on commit 84da38d

Please sign in to comment.