Skip to content

Commit 68709ee

Browse files
committed
fix: Avoid locking on app lifecycle change
1 parent 2feac0b commit 68709ee

File tree

5 files changed

+101
-39
lines changed

5 files changed

+101
-39
lines changed

pkg/backend/geth_backend.go

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2089,20 +2089,26 @@ func (b *StatusBackend) getVerifiedWalletAccount(address, password string) (*gen
20892089
// AppStateChange handles app state changes (background/foreground).
20902090
// state values: see https://facebook.github.io/react-native/docs/appstate.html
20912091
func (b *StatusBackend) AppStateChange(state AppState) {
2092-
b.mu.Lock()
2093-
defer b.mu.Unlock()
2094-
20952092
if !state.IsValid() {
20962093
b.logger.Warn("invalid app state, not reporting app state change", zap.Any("state", state))
20972094
return
20982095
}
20992096

2100-
b.appState = state
2097+
b.mu.Lock()
2098+
defer b.mu.Unlock()
21012099

2100+
b.appState = state
21022101
if b.statusNode == nil {
21032102
b.logger.Warn("statusNode nil, applying app state change without running node")
21042103
}
21052104

2105+
if state == AppStateForeground && b.lifecycleState == AppLifecycleRunning {
2106+
return
2107+
}
2108+
if state != AppStateForeground && b.lifecycleState == AppLifecyclePausedBackground {
2109+
return
2110+
}
2111+
21062112
if state == AppStateForeground {
21072113
if err := b.resumeLocked(); err != nil {
21082114
b.logger.Warn("failed to resume backend on app foreground", zap.Error(err))

pkg/backend/pause_play.go

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package backend
33
import (
44
"fmt"
55

6+
"github.com/status-im/status-go/pkg/backend/node"
67
"github.com/status-im/status-go/protocol"
78
)
89

@@ -18,59 +19,66 @@ const (
1819

1920
func (s AppLifecycleState) String() string { return string(s) }
2021

22+
// LifecycleState returns the current backend lifecycle state.
2123
func (b *StatusBackend) LifecycleState() AppLifecycleState {
2224
b.mu.Lock()
2325
defer b.mu.Unlock()
2426
return b.lifecycleState
2527
}
2628

29+
// pauseLocked must be called with b.mu held.
2730
func (b *StatusBackend) pauseLocked() error {
2831
if b.lifecycleState == AppLifecycleStopped {
2932
return nil
3033
}
3134
if b.lifecycleState == AppLifecyclePausedBackground {
3235
return nil
3336
}
34-
if b.statusNode == nil || !b.statusNode.IsRunning() {
37+
sn := b.statusNode
38+
39+
if sn == nil || !sn.IsRunning() {
3540
b.lifecycleState = AppLifecycleStopped
3641
return nil
3742
}
38-
if messenger := b.currentMessengerLocked(); messenger != nil {
43+
if messenger := b.currentMessenger(sn); messenger != nil {
3944
messenger.ToBackground()
4045
}
41-
if err := b.statusNode.PauseBackground(); err != nil {
46+
if err := sn.PauseBackground(); err != nil {
4247
return fmt.Errorf("pause background: %w", err)
4348
}
4449
b.lifecycleState = AppLifecyclePausedBackground
4550
return nil
4651
}
4752

53+
// resumeLocked must be called with b.mu held.
4854
func (b *StatusBackend) resumeLocked() error {
4955
if b.lifecycleState == AppLifecycleStopped {
5056
return nil
5157
}
5258
if b.lifecycleState == AppLifecycleRunning {
5359
return nil
5460
}
55-
if b.statusNode == nil || !b.statusNode.IsRunning() {
61+
sn := b.statusNode
62+
63+
if sn == nil || !sn.IsRunning() {
5664
b.lifecycleState = AppLifecycleStopped
5765
return nil
5866
}
59-
if messenger := b.currentMessengerLocked(); messenger != nil {
67+
if messenger := b.currentMessenger(sn); messenger != nil {
6068
messenger.ToForeground()
6169
}
6270

6371
b.lifecycleState = AppLifecycleResumingForeground
64-
if err := b.statusNode.ResumeForeground(); err != nil {
72+
if err := sn.ResumeForeground(); err != nil {
6573
return fmt.Errorf("resume foreground: %w", err)
6674
}
6775
b.lifecycleState = AppLifecycleRunning
6876
return nil
6977
}
7078

71-
func (b *StatusBackend) currentMessengerLocked() *protocol.Messenger {
72-
if b.statusNode == nil || b.statusNode.WakuV2ExtService() == nil {
79+
func (b *StatusBackend) currentMessenger(sn *node.StatusNode) *protocol.Messenger {
80+
if sn == nil || sn.WakuV2ExtService() == nil {
7381
return nil
7482
}
75-
return b.statusNode.WakuV2ExtService().Messenger()
83+
return sn.WakuV2ExtService().Messenger()
7684
}

pkg/messaging/layers/reliability/datasync/transport.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func SetPausedBackground(paused bool) {
3333
func currentOffsetToSecond() uint64 {
3434
datasyncTickerMutex.RLock()
3535
defer datasyncTickerMutex.RUnlock()
36-
return uint64(time.Second / DatasyncTicker)
36+
return uint64(math.Ceil(float64(time.Second) / float64(DatasyncTicker)))
3737
}
3838

3939
type NodeTransport struct {

server/server.go

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/url"
1111
"strconv"
1212
"sync"
13+
"sync/atomic"
1314
"syscall"
1415
"time"
1516

@@ -29,7 +30,7 @@ type Config struct {
2930
}
3031

3132
type Server struct {
32-
lifecycleMu sync.Mutex
33+
mu sync.Mutex
3334

3435
listener net.Listener
3536
server *http.Server
@@ -41,8 +42,8 @@ type Server struct {
4142
// address is the host and port the server is listening on
4243
address *net.TCPAddr
4344

44-
// isRunning is true if the server was started and is running
45-
isRunning bool
45+
// running is true if the server was started and Serve is active; read via IsRunning().
46+
running atomic.Bool
4647

4748
// cachedPort stores the port from the first successful bind when AddrPort used port 0 (ephemeral).
4849
// Reused on pause/resume so URLs remain valid across ToBackground/ToForeground cycles.
@@ -159,13 +160,13 @@ func (s *Server) serve(currentServer *http.Server, currentListener net.Listener)
159160
defer common.LogOnPanic()
160161

161162
defer func() {
162-
s.lifecycleMu.Lock()
163-
defer s.lifecycleMu.Unlock()
163+
s.mu.Lock()
164+
defer s.mu.Unlock()
164165

165166
// If a newer Start() already replaced server/listener, do not clobber
166167
// the latest running instance state.
167168
if s.server == currentServer && s.listener == currentListener {
168-
s.isRunning = false
169+
s.running.Store(false)
169170
s.address = nil
170171
}
171172
}()
@@ -199,10 +200,10 @@ func (s *Server) applyHandlers() {
199200
}
200201

201202
func (s *Server) Start() error {
202-
s.lifecycleMu.Lock()
203-
defer s.lifecycleMu.Unlock()
203+
s.mu.Lock()
204+
defer s.mu.Unlock()
204205

205-
if s.isRunning {
206+
if s.running.Load() {
206207
return nil
207208
}
208209

@@ -217,31 +218,31 @@ func (s *Server) Start() error {
217218

218219
// Mark running synchronously to avoid pause/play races where ToBackground
219220
// can run before serve() goroutine has a chance to set the state.
220-
s.isRunning = true
221+
s.running.Store(true)
221222
go s.serve(s.server, s.listener)
222223
return nil
223224
}
224225

225226
func (s *Server) Stop() error {
226-
s.lifecycleMu.Lock()
227+
s.mu.Lock()
227228
s.StopTimeout()
228-
if !s.isRunning || s.server == nil {
229-
s.lifecycleMu.Unlock()
229+
if !s.running.Load() || s.server == nil {
230+
s.mu.Unlock()
230231
return nil
231232
}
232233

233234
// Capture the current instance and release the lock before Shutdown.
234235
// Shutdown waits for Serve() to return, and Serve() may update state in
235236
// its defer path under the same mutex.
236237
currentServer := s.server
237-
s.isRunning = false
238-
s.lifecycleMu.Unlock()
238+
s.running.Store(false)
239+
s.mu.Unlock()
239240

240241
return currentServer.Shutdown(context.Background())
241242
}
242243

243244
func (s *Server) IsRunning() bool {
244-
return s.isRunning
245+
return s.running.Load()
245246
}
246247

247248
func (s *Server) ToForeground() {

services/connector/service_test.go

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package connector
22

33
import (
4+
"net"
5+
"strconv"
46
"testing"
57
"time"
68

79
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
811
)
912

1013
func TestNewService(t *testing.T) {
@@ -57,20 +60,64 @@ func TestService_APIs(t *testing.T) {
5760
func TestService_PauseResumeBackground(t *testing.T) {
5861
state := setupTests(t)
5962

63+
// Pause/resume only affects the WS listener path; use a pre-assigned port so we can dial it.
64+
host, port := freeLocalTCPPort(t)
65+
addr := net.JoinHostPort(host, strconv.Itoa(port))
66+
state.service.config.WSEnabled = true
67+
state.service.config.WSHost = host
68+
state.service.config.WSPort = port
69+
70+
t.Cleanup(func() { _ = state.service.Stop() })
71+
6072
err := state.service.Start()
61-
assert.NoError(t, err)
73+
require.NoError(t, err)
74+
require.NotNil(t, state.service.wsServer)
75+
waitUntilTCPAccepts(t, addr)
6276

6377
err = state.service.PauseBackground()
64-
assert.NoError(t, err)
65-
assert.True(t, state.service.paused)
78+
require.NoError(t, err)
79+
require.True(t, state.service.paused)
80+
waitUntilTCPRefused(t, addr)
6681

6782
err = state.service.ResumeForeground()
68-
assert.NoError(t, err)
69-
assert.False(t, state.service.paused)
70-
71-
// Give the listener goroutine a small window to bind.
72-
time.Sleep(50 * time.Millisecond)
83+
require.NoError(t, err)
84+
require.False(t, state.service.paused)
85+
waitUntilTCPAccepts(t, addr)
7386

7487
err = state.service.Stop()
75-
assert.NoError(t, err)
88+
require.NoError(t, err)
89+
}
90+
91+
// freeLocalTCPPort reserves an ephemeral TCP port on loopback for the test process.
92+
// There is a small race where another process could bind between Close and Start; acceptable for tests.
93+
func freeLocalTCPPort(t *testing.T) (host string, port int) {
94+
t.Helper()
95+
ln, err := net.Listen("tcp", "127.0.0.1:0")
96+
require.NoError(t, err)
97+
tcpAddr := ln.Addr().(*net.TCPAddr)
98+
require.NoError(t, ln.Close())
99+
return tcpAddr.IP.String(), tcpAddr.Port
100+
}
101+
102+
func waitUntilTCPAccepts(t *testing.T, addr string) {
103+
t.Helper()
104+
require.Eventually(t, func() bool {
105+
c, err := net.DialTimeout("tcp", addr, 50*time.Millisecond)
106+
if err != nil {
107+
return false
108+
}
109+
_ = c.Close()
110+
return true
111+
}, 5*time.Second, 10*time.Millisecond, "expected TCP accept on %s", addr)
112+
}
113+
114+
func waitUntilTCPRefused(t *testing.T, addr string) {
115+
t.Helper()
116+
require.Eventually(t, func() bool {
117+
c, err := net.DialTimeout("tcp", addr, 50*time.Millisecond)
118+
if c != nil {
119+
_ = c.Close()
120+
}
121+
return err != nil
122+
}, 5*time.Second, 10*time.Millisecond, "expected no listener on %s after pause", addr)
76123
}

0 commit comments

Comments
 (0)