@@ -2,9 +2,12 @@ package di
22
33import (
44 "context"
5+ "os"
6+ "time"
57
68 "github.com/boreq/errors"
79 "github.com/hashicorp/go-multierror"
10+ "github.com/planetary-social/nos-event-service/internal/goroutine"
811 "github.com/planetary-social/nos-event-service/internal/logging"
912 "github.com/planetary-social/nos-event-service/internal/migrations"
1013 "github.com/planetary-social/nos-event-service/service/adapters/sqlite"
@@ -72,6 +75,8 @@ func (s Service) ExecuteMigrations(ctx context.Context) error {
7275 return s .migrationsRunner .Run (ctx , s .migrations , s .migrationsProgressCallback )
7376}
7477
78+ const shutdownTimeout = 30 * time .Second
79+
7580func (s Service ) Run (ctx context.Context ) error {
7681 ctx , cancel := context .WithCancel (ctx )
7782 defer cancel ()
@@ -81,66 +86,83 @@ func (s Service) Run(ctx context.Context) error {
8186
8287 runners ++
8388 // Serve http
84- go func () {
85- errCh <- errors .Wrap (s .server .ListenAndServe (ctx ), "server error" )
86- }( )
89+ goroutine . Run ( s . logger , errCh , func () error {
90+ return errors .Wrap (s .server .ListenAndServe (ctx ), "server error" )
91+ })
8792
8893 // Fetch events from the database relays and send them to the in memory pubsub
8994 runners ++
90- go func () {
91- errCh <- errors .Wrap (s .downloader .Run (ctx ), "downloader error" )
92- }( )
95+ goroutine . Run ( s . logger , errCh , func () error {
96+ return errors .Wrap (s .downloader .Run (ctx ), "downloader error" )
97+ })
9398
9499 // Subscribe to the in memory pubsub of events, emit a NewSaveReceivedEvent
95100 // command that will make some checks on the event, save it if the check
96101 // passes and emit a EventSavedEvent to the sqlite pubsub.
97102 runners ++
98- go func () {
99- errCh <- errors .Wrap (s .receivedEventSubscriber .Run (ctx ), "received event subscriber error" )
100- }( )
103+ goroutine . Run ( s . logger , errCh , func () error {
104+ return errors .Wrap (s .receivedEventSubscriber .Run (ctx ), "received event subscriber error" )
105+ })
101106
102107 // Subscribe to saved events in the database. This uses the sqlite pubsub. This triggers:
103108 // - analysis to extract new relays and store them in db. They will be used by the downloader.
104109 // - analysis to store pubkeys and store them in the db (contacts_followees, pubkeys, contacts_events). This will be used by the downloader.
105110 // - publish to watermill pubsub
106111 // - may publish the event in wss://relay.nos.social if they are metadata related
107112 runners ++
108- go func () {
109- errCh <- errors .Wrap (s .eventSavedEventSubscriber .Run (ctx ), "event saved subscriber error" )
110- }( )
113+ goroutine . Run ( s . logger , errCh , func () error {
114+ return errors .Wrap (s .eventSavedEventSubscriber .Run (ctx ), "event saved subscriber error" )
115+ })
111116
112117 // The metrics timer collects metrics from the app.
113118 runners ++
114- go func () {
115- errCh <- errors .Wrap (s .metricsTimer .Run (ctx ), "metrics timer error" )
116- }( )
119+ goroutine . Run ( s . logger , errCh , func () error {
120+ return errors .Wrap (s .metricsTimer .Run (ctx ), "metrics timer error" )
121+ })
117122
118123 // Sqlite transaction runner
119124 runners ++
120- go func () {
121- errCh <- errors .Wrap (s .transactionRunner .Run (ctx ), "transaction runner error" )
122- }( )
125+ goroutine . Run ( s . logger , errCh , func () error {
126+ return errors .Wrap (s .transactionRunner .Run (ctx ), "transaction runner error" )
127+ })
123128
124129 // The task scheduler creates sequential time window based tasks that
125130 // contain filters to be applied to each relay to fetch the events we want.
126131 // Event downloaders subscribe to this.
127132 runners ++
128- go func () {
129- errCh <- errors .Wrap (s .taskScheduler .Run (ctx ), "task scheduler error" )
130- }( )
133+ goroutine . Run ( s . logger , errCh , func () error {
134+ return errors .Wrap (s .taskScheduler .Run (ctx ), "task scheduler error" )
135+ })
131136
132137 // The cleanup timer periodically removes processed events older than retention period
133138 runners ++
134- go func () {
135- errCh <- errors .Wrap (s .cleanupTimer .Run (ctx ), "cleanup timer error" )
136- }()
139+ goroutine .Run (s .logger , errCh , func () error {
140+ return errors .Wrap (s .cleanupTimer .Run (ctx ), "cleanup timer error" )
141+ })
142+
143+ // Wait for the first runner to exit.
144+ firstErr := <- errCh
145+ s .logger .Error ().WithError (firstErr ).Message ("first runner terminated, shutting down" )
146+ cancel ()
137147
148+ // Collect remaining errors with a timeout to prevent zombie processes.
138149 var compoundErr error
139- for i := 0 ; i < runners ; i ++ {
140- err := errors .Wrap (<- errCh , "error returned by runner" )
141- s .logger .Error ().WithError (err ).Message ("runner terminated" )
142- compoundErr = multierror .Append (compoundErr , err )
143- cancel ()
150+ compoundErr = multierror .Append (compoundErr , errors .Wrap (firstErr , "error returned by runner" ))
151+
152+ timer := time .NewTimer (shutdownTimeout )
153+ defer timer .Stop ()
154+
155+ for i := 1 ; i < runners ; i ++ {
156+ select {
157+ case err := <- errCh :
158+ err = errors .Wrap (err , "error returned by runner" )
159+ s .logger .Error ().WithError (err ).Message ("runner terminated" )
160+ compoundErr = multierror .Append (compoundErr , err )
161+ case <- timer .C :
162+ s .logger .Error ().Message ("shutdown timeout exceeded, forcing exit" )
163+ os .Exit (1 )
164+ }
144165 }
166+
145167 return compoundErr
146168}
0 commit comments