~bigbes/lethe

ref: e108b3e06984223f9489e5dcf1f22ed60ae8adb7 lethe/internal/platform/health/steward_unwind_test.go -rw-r--r-- 2.5 KiB
e108b3e0 — Eugene Blikh feat(session): list and detail JSON API with filters a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package health_test

// This is the Phase 4 steward unwind canary. It verifies whether
// steward.Manager invokes Destroy on already-initialized siblings when a
// later component's Init returns an error. The lifecycle design in the
// lethe-server task assumes it does; if not, an explicit unwind step has to
// be added in main.go (Phase 9).
//
// The test is deliberately placed in package `health_test` (next to the
// health package) so it can import steward without polluting the health
// package and without introducing a brand-new package directory just for
// the canary. See the Phase 4 plan, point 16.
//
// IMPORTANT: this test is allowed to fail. A failure here is the very signal
// the dispatcher needs to plan an explicit unwind. Do NOT mark it Skip; do
// NOT add a workaround in this phase.

import (
	"context"
	"errors"
	"testing"

	"go.bigb.es/auxilia/steward"
)

// recordingService records whether Init and Destroy were called. It has no
// dependencies, so it can sit anywhere in the start order.
type recordingService struct {
	initCalled    bool
	destroyCalled bool
}

func (r *recordingService) Init(_ context.Context) error {
	r.initCalled = true
	return nil
}

func (r *recordingService) Destroy(_ context.Context) error {
	r.destroyCalled = true
	return nil
}

// failingService errors out of Init. It is registered after recordingService
// so that recordingService is already initialized at the point of failure.
type failingService struct{}

var errFailing = errors.New("failingService.Init: intentional failure")

func (f *failingService) Init(_ context.Context) error { return errFailing }

func TestStewardUnwindsOnInitFailure(t *testing.T) {
	rec := &recordingService{}
	fail := &failingService{}

	mgr := steward.NewManager()
	mgr.AddComponent(context.Background(),
		steward.MustServiceAsset(rec, steward.Root(), steward.IgnoreUnused()),
		steward.MustServiceAsset(fail, steward.Root(), steward.IgnoreUnused()),
	)

	if err := mgr.Inject(context.Background()); err != nil {
		t.Fatalf("Inject: %v", err)
	}

	initErr := mgr.Init(context.Background())
	if initErr == nil {
		t.Fatalf("expected Init to surface failingService error, got nil")
	}
	if !rec.initCalled {
		t.Fatalf("recordingService.Init was never called — registration order assumption broken")
	}
	if !rec.destroyCalled {
		// THE FINDING. Steward did not unwind. main.go must call Destroy on
		// every initialized sibling itself when Init fails.
		t.Fatalf("steward did NOT call recordingService.Destroy after sibling Init failed; explicit unwind required in main")
	}
}