Skip to content

Commit f73302a

Browse files
committed
feat: pause background work when app is in background
Introduce a lifecycle-based pause/resume mechanism to reduce non-essential work when the app goes to background, improving battery and resource usage. - Add pkg/messaging/lifecycle for SubscribePausedBackground/SetPausedBackground - Add backend pause/play (pauseLocked/resumeLocked) driven by AppStateChange - StatusNode.PauseBackground/ResumeForeground coordinate services via backgroundLifecycle interface - Messenger.ToForeground/ToBackground broadcast to lifecycle, datasync, and push notification client - Pause tickers/loops in: centralizedmetrics, ipfs, rpc limiter, ntp, backup, transport, envelopes monitor, connector, wallet, newsfeed, ext - Simplify mobile AppStateChange (remove AppStateChangeV2, single API) - RPC client: clean up limiters on Stop
1 parent 9e77ec1 commit f73302a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1400
-164
lines changed

internal/centralizedmetrics/metrics.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
gocommon "github.com/status-im/status-go/common"
1111
"github.com/status-im/status-go/internal/centralizedmetrics/common"
1212
"github.com/status-im/status-go/internal/centralizedmetrics/providers"
13+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
1314
)
1415

1516
const defaultPollInterval = 10 * time.Second
@@ -74,11 +75,31 @@ func (s *MetricService) Start() {
7475
go func() {
7576
defer gocommon.LogOnPanic()
7677
defer s.wg.Done()
78+
79+
sub := messaginglifecycle.SubscribePausedBackground()
80+
defer sub.Unsubscribe()
81+
82+
paused := <-sub.C()
83+
var tickerC <-chan time.Time
84+
if !paused {
85+
tickerC = s.ticker.C
86+
}
87+
7788
for {
7889
select {
7990
case <-s.done:
8091
return
81-
case <-s.ticker.C:
92+
case pausedState, ok := <-sub.C():
93+
if !ok {
94+
return
95+
}
96+
paused = pausedState
97+
if paused {
98+
tickerC = nil
99+
} else {
100+
tickerC = s.ticker.C
101+
}
102+
case <-tickerC:
82103
s.processMetrics()
83104
}
84105
}

internal/centralizedmetrics/metrics_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/status-im/status-go/internal/centralizedmetrics/common"
1313
testutils2 "github.com/status-im/status-go/internal/testutils"
14+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
1415
)
1516

1617
var testMetric = common.Metric{ID: "user-id", EventName: "test-name", EventValue: map[string]interface{}{"test-name": "test-value"}, Platform: "android", AppVersion: "2.30.0"}
@@ -221,3 +222,23 @@ func TestServiceEnsureStarted(t *testing.T) {
221222
require.NoError(t, err)
222223
require.True(t, service.started)
223224
}
225+
226+
func TestMetricServicePausesAndResumesByLifecycle(t *testing.T) {
227+
messaginglifecycle.SetPausedBackground(true)
228+
defer messaginglifecycle.SetPausedBackground(false)
229+
230+
repository := &TestMetricRepository{}
231+
processor := &TestMetricProcessor{}
232+
service := newMetricService(t, repository, processor, 40*time.Millisecond)
233+
service.Start()
234+
defer service.Stop()
235+
236+
require.NoError(t, service.AddMetric(testMetric))
237+
time.Sleep(160 * time.Millisecond)
238+
require.Len(t, processor.processedMetrics, 0)
239+
240+
messaginglifecycle.SetPausedBackground(false)
241+
require.Eventually(t, func() bool {
242+
return len(processor.processedMetrics) > 0
243+
}, time.Second, 20*time.Millisecond)
244+
}

internal/ipfs/ipfs.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/status-im/status-go/common"
2020
"github.com/status-im/status-go/internal/logutils"
2121
"github.com/status-im/status-go/params"
22+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
2223
)
2324

2425
const maxRequestsPerSecond = 3
@@ -100,15 +101,39 @@ func (d *Downloader) taskDispatcher() {
100101
defer common.LogOnPanic()
101102
ticker := time.NewTicker(time.Second / maxRequestsPerSecond)
102103
defer ticker.Stop()
104+
sub := messaginglifecycle.SubscribePausedBackground()
105+
defer sub.Unsubscribe()
106+
107+
paused := <-sub.C()
108+
var tickerC <-chan time.Time
109+
if !paused {
110+
tickerC = ticker.C
111+
}
103112

104113
for {
105-
<-ticker.C
106-
request, ok := <-d.inputTaskChan
107-
if !ok {
114+
select {
115+
case <-d.quit:
108116
return
117+
case pausedState, ok := <-sub.C():
118+
if !ok {
119+
return
120+
}
121+
paused = pausedState
122+
if paused {
123+
tickerC = nil
124+
} else {
125+
tickerC = ticker.C
126+
}
127+
case <-tickerC:
128+
select {
129+
case request, ok := <-d.inputTaskChan:
130+
if !ok {
131+
return
132+
}
133+
d.rateLimiterChan <- request
134+
default:
135+
}
109136
}
110-
d.rateLimiterChan <- request
111-
112137
}
113138
}
114139

internal/ipfs/ipfs_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ipfs
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/require"
8+
9+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
10+
)
11+
12+
func TestTaskDispatcherPausesAndResumesByLifecycle(t *testing.T) {
13+
messaginglifecycle.SetPausedBackground(true)
14+
defer messaginglifecycle.SetPausedBackground(false)
15+
16+
d := &Downloader{
17+
inputTaskChan: make(chan taskRequest, 1),
18+
rateLimiterChan: make(chan taskRequest, 1),
19+
quit: make(chan struct{}, 1),
20+
}
21+
22+
go d.taskDispatcher()
23+
defer close(d.quit)
24+
25+
req := taskRequest{cid: "cid-1", doneChan: make(chan taskResponse, 1)}
26+
d.inputTaskChan <- req
27+
28+
select {
29+
case <-d.rateLimiterChan:
30+
t.Fatal("task was dispatched while paused")
31+
case <-time.After(450 * time.Millisecond):
32+
}
33+
34+
messaginglifecycle.SetPausedBackground(false)
35+
36+
select {
37+
case got := <-d.rateLimiterChan:
38+
require.Equal(t, req.cid, got.cid)
39+
case <-time.After(2 * time.Second):
40+
t.Fatal("task was not dispatched after resume")
41+
}
42+
}

internal/rpc/chain/rpclimiter/rpc_limiter.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
gocommon "github.com/status-im/status-go/common"
1414
"github.com/status-im/status-go/internal/logutils"
15+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
1516
)
1617

1718
const (
@@ -260,9 +261,29 @@ func (rl *RPCRpsLimiter) start() {
260261
ticker := time.NewTicker(tickerInterval)
261262
go func() {
262263
defer gocommon.LogOnPanic()
264+
sub := messaginglifecycle.SubscribePausedBackground()
265+
defer sub.Unsubscribe()
266+
267+
paused := <-sub.C()
268+
var tickerC <-chan time.Time
269+
if !paused {
270+
tickerC = ticker.C
271+
}
272+
263273
for {
264274
select {
265-
case <-ticker.C:
275+
case pausedState, ok := <-sub.C():
276+
if !ok {
277+
ticker.Stop()
278+
return
279+
}
280+
paused = pausedState
281+
if paused {
282+
tickerC = nil
283+
} else {
284+
tickerC = ticker.C
285+
}
286+
case <-tickerC:
266287
{
267288
rl.requestsMadeWithinSecondMutex.Lock()
268289
oldrequestsMadeWithinSecond := rl.requestsMadeWithinSecond

internal/rpc/chain/rpclimiter/rpc_limiter_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/google/uuid"
78
"github.com/stretchr/testify/require"
9+
10+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
811
)
912

1013
func setupTest() (*InMemRequestsMapStorage, RequestLimiter) {
@@ -193,3 +196,36 @@ func TestAllowWhenLimitNotReachedForInfinitePeriod(t *testing.T) {
193196
// Verify the result
194197
require.True(t, allow)
195198
}
199+
200+
func TestRPCRpsLimiterStartPausesAndResumesReleases(t *testing.T) {
201+
messaginglifecycle.SetPausedBackground(true)
202+
defer messaginglifecycle.SetPausedBackground(false)
203+
204+
waitCh := make(chan bool, 1)
205+
rl := &RPCRpsLimiter{
206+
uuid: uuid.New(),
207+
maxRequestsPerSecond: 1,
208+
requestsMadeWithinSecond: 1,
209+
callersOnWaitForRequests: []callerOnWait{
210+
{requests: 1, ch: waitCh},
211+
},
212+
quit: make(chan bool),
213+
}
214+
215+
rl.start()
216+
defer rl.Stop()
217+
218+
select {
219+
case <-waitCh:
220+
t.Fatal("limiter released callers while paused")
221+
case <-time.After(tickerInterval + 200*time.Millisecond):
222+
}
223+
224+
messaginglifecycle.SetPausedBackground(false)
225+
226+
select {
227+
case <-waitCh:
228+
case <-time.After(2 * tickerInterval):
229+
t.Fatal("limiter did not release callers after resume")
230+
}
231+
}

internal/rpc/client.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,15 @@ func (c *Client) Stop() {
145145
c.signalsTransmitter.Stop()
146146
c.networkManager.Stop()
147147

148+
c.rpsLimiterMutex.Lock()
149+
for key, limiter := range c.limiterPerProvider {
150+
if limiter != nil {
151+
limiter.Stop()
152+
}
153+
delete(c.limiterPerProvider, key)
154+
}
155+
c.rpsLimiterMutex.Unlock()
156+
148157
c.rpcClientsMutex.Lock()
149158
for _, client := range c.rpcClients {
150159
client.Close()

internal/timesource/ntp.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/status-im/status-go/common"
1515
"github.com/status-im/status-go/internal/logutils"
16+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
1617
)
1718

1819
const (
@@ -221,14 +222,52 @@ func (s *ntpTimeSource) runPeriodically(ctx context.Context, fn func() error, st
221222
}
222223
go func() {
223224
defer common.LogOnPanic()
225+
sub := messaginglifecycle.SubscribePausedBackground()
226+
defer sub.Unsubscribe()
227+
228+
paused := <-sub.C()
229+
if paused && period != s.slowNTPSyncPeriod {
230+
period = s.slowNTPSyncPeriod
231+
}
232+
233+
timer := time.NewTimer(period)
234+
defer timer.Stop()
235+
236+
resetTimer := func(d time.Duration) {
237+
if !timer.Stop() {
238+
select {
239+
case <-timer.C:
240+
default:
241+
}
242+
}
243+
timer.Reset(d)
244+
}
245+
224246
for {
225247
select {
226-
case <-time.After(period):
248+
case pausedState, ok := <-sub.C():
249+
if !ok {
250+
return
251+
}
252+
paused = pausedState
253+
if paused && period != s.slowNTPSyncPeriod {
254+
period = s.slowNTPSyncPeriod
255+
}
256+
resetTimer(period)
257+
case <-timer.C:
258+
if paused {
259+
resetTimer(period)
260+
continue
261+
}
227262
if err := fn(); err == nil {
228263
period = s.slowNTPSyncPeriod
229264
} else if period != s.slowNTPSyncPeriod {
230265
period = s.fastNTPSyncPeriod
231266
}
267+
if paused && period != s.slowNTPSyncPeriod {
268+
period = s.slowNTPSyncPeriod
269+
}
270+
resetTimer(period)
232271

233272
case <-ctx.Done():
234273
return

internal/timesource/ntp_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"github.com/beevik/ntp"
1111
"github.com/stretchr/testify/assert"
1212
"github.com/stretchr/testify/require"
13+
14+
messaginglifecycle "github.com/status-im/status-go/pkg/messaging/lifecycle"
1315
)
1416

1517
const (
@@ -287,6 +289,39 @@ func TestGetCurrentTimeInMillis(t *testing.T) {
287289
ts.Stop()
288290
}
289291

292+
func TestRunPeriodicallyPausesAndResumesByLifecycle(t *testing.T) {
293+
messaginglifecycle.SetPausedBackground(true)
294+
defer messaginglifecycle.SetPausedBackground(false)
295+
296+
source := &ntpTimeSource{
297+
fastNTPSyncPeriod: 20 * time.Millisecond,
298+
slowNTPSyncPeriod: 120 * time.Millisecond,
299+
}
300+
301+
ctx, cancel := context.WithCancel(context.Background())
302+
defer cancel()
303+
304+
hitsCh := make(chan struct{}, 4)
305+
source.runPeriodically(ctx, func() error {
306+
hitsCh <- struct{}{}
307+
return nil
308+
}, false)
309+
310+
select {
311+
case <-hitsCh:
312+
t.Fatal("periodic function ran while paused")
313+
case <-time.After(90 * time.Millisecond):
314+
}
315+
316+
messaginglifecycle.SetPausedBackground(false)
317+
318+
select {
319+
case <-hitsCh:
320+
case <-time.After(600 * time.Millisecond):
321+
t.Fatal("periodic function did not run after resume")
322+
}
323+
}
324+
290325
func TestGetCurrentTimeOffline(t *testing.T) {
291326
// covers https://github.com/status-im/status-desktop/issues/12691
292327
ts := &ntpTimeSource{

0 commit comments

Comments
 (0)