Skip to content

Commit 1d8cca9

Browse files
feat: implemented a new parent lifecycle to support shutdown when parent application shuts down
1 parent 2647b48 commit 1d8cca9

4 files changed

Lines changed: 107 additions & 20 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package lifecycle
2+
3+
import (
4+
"io"
5+
"os"
6+
"os/signal"
7+
"sync"
8+
)
9+
10+
const ParentStdinWatchEnv = "HORNETS_PARENT_STDIN_WATCH"
11+
12+
// StopChannel closes when an OS shutdown signal arrives or, when explicitly
13+
// enabled, when the supervising parent's stdin pipe reaches EOF. Services and
14+
// standalone invocations retain their existing signal-only lifecycle.
15+
func StopChannel(parent io.Reader, watchParent bool, shutdownSignals ...os.Signal) <-chan struct{} {
16+
stop := make(chan struct{})
17+
var once sync.Once
18+
requestStop := func() {
19+
once.Do(func() { close(stop) })
20+
}
21+
22+
signals := make(chan os.Signal, 1)
23+
signal.Notify(signals, shutdownSignals...)
24+
go func() {
25+
select {
26+
case <-signals:
27+
requestStop()
28+
case <-stop:
29+
}
30+
signal.Stop(signals)
31+
}()
32+
33+
if watchParent {
34+
go func() {
35+
_, _ = io.Copy(io.Discard, parent)
36+
requestStop()
37+
}()
38+
}
39+
40+
return stop
41+
}
42+
43+
// WatchParentStdin reports whether this console process was launched under the
44+
// Nosis session supervisor. It is deliberately opt-in so an ordinary closed
45+
// terminal stdin never stops a service or standalone relay.
46+
func WatchParentStdin() bool {
47+
return os.Getenv(ParentStdinWatchEnv) == "1"
48+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package lifecycle
2+
3+
import (
4+
"os"
5+
"testing"
6+
"time"
7+
)
8+
9+
func TestStopChannelClosesWhenParentPipeCloses(t *testing.T) {
10+
reader, writer, err := os.Pipe()
11+
if err != nil {
12+
t.Fatal(err)
13+
}
14+
defer reader.Close()
15+
16+
stop := StopChannel(reader, true, os.Interrupt)
17+
if err := writer.Close(); err != nil {
18+
t.Fatal(err)
19+
}
20+
21+
select {
22+
case <-stop:
23+
case <-time.After(time.Second):
24+
t.Fatal("parent pipe EOF did not close stop channel")
25+
}
26+
}
27+
28+
func TestStopChannelIgnoresClosedParentWhenWatchDisabled(t *testing.T) {
29+
reader, writer, err := os.Pipe()
30+
if err != nil {
31+
t.Fatal(err)
32+
}
33+
defer reader.Close()
34+
35+
stop := StopChannel(reader, false, os.Interrupt)
36+
if err := writer.Close(); err != nil {
37+
t.Fatal(err)
38+
}
39+
40+
select {
41+
case <-stop:
42+
t.Fatal("closed parent pipe stopped an unsupervised process")
43+
case <-time.After(50 * time.Millisecond):
44+
}
45+
}

services/server/port/main.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import (
44
"context"
55
"flag"
66
"os"
7-
"os/signal"
87
"syscall"
98

109
"github.com/HORNET-Storage/hornet-storage/lib/logging"
1110
"github.com/HORNET-Storage/hornet-storage/services/server/core"
11+
"github.com/HORNET-Storage/hornet-storage/services/server/lifecycle"
1212
)
1313

1414
var (
@@ -30,15 +30,12 @@ func init() {
3030
}
3131

3232
func main() {
33-
// Convert OS kill signals into the core stop channel
34-
sigs := make(chan os.Signal, 1)
35-
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
36-
37-
stop := make(chan struct{})
38-
go func() {
39-
<-sigs
40-
close(stop)
41-
}()
33+
stop := lifecycle.StopChannel(
34+
os.Stdin,
35+
lifecycle.WatchParentStdin(),
36+
syscall.SIGINT,
37+
syscall.SIGTERM,
38+
)
4239

4340
if err := core.Run(context.Background(), core.Options{
4441
CompactDB: *compactDB,

services/server/windows/main.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"flag"
88
"fmt"
99
"os"
10-
"os/signal"
1110
"path/filepath"
1211
"strings"
1312
"syscall"
@@ -16,6 +15,7 @@ import (
1615

1716
"github.com/HORNET-Storage/hornet-storage/lib/logging"
1817
"github.com/HORNET-Storage/hornet-storage/services/server/core"
18+
"github.com/HORNET-Storage/hornet-storage/services/server/lifecycle"
1919
)
2020

2121
// serviceName is the Windows service identity shared by the installer
@@ -77,15 +77,12 @@ func runConsole() {
7777
// Initialize config and logging before entering the shared run lifecycle
7878
core.Initialize()
7979

80-
// Convert OS kill signals into the core stop channel
81-
sigs := make(chan os.Signal, 1)
82-
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
83-
84-
stop := make(chan struct{})
85-
go func() {
86-
<-sigs
87-
close(stop)
88-
}()
80+
stop := lifecycle.StopChannel(
81+
os.Stdin,
82+
lifecycle.WatchParentStdin(),
83+
syscall.SIGINT,
84+
syscall.SIGTERM,
85+
)
8986

9087
if err := core.Run(context.Background(), options(stop)); err != nil {
9188
logging.Fatalf("Relay exited with error: %v", err)

0 commit comments

Comments
 (0)