Pull request 2646: 8359-fix-deadlock

Updates #8359.

Squashed commit of the following:

commit 3242b00261ddcf37632999a992e2b81eae5a7f95
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Thu Apr 30 14:10:13 2026 +0300

    stats: imp code

commit b7bf300f8744b53f6cd49ce37ebd36c970da5630
Author: Eugene Burkov <E.Burkov@AdGuard.COM>
Date:   Wed Apr 29 22:28:59 2026 +0300

    all: fix deadlock
This commit is contained in:
Eugene Burkov 2026-05-04 13:00:27 +00:00
parent 9e3d913e72
commit 95b2421314
7 changed files with 21 additions and 14 deletions

View file

@ -28,6 +28,8 @@ NOTE: Add new changes BELOW THIS COMMENT.
### Fixed
- Statistics database deadlock ([#8359]).
- Translated labels on the DNS settings pages not updating after changing the UI language.
- Dashboard charts now correctly display lower query counts ([#6823]).

View file

@ -45,4 +45,4 @@ describe('checkRedirect', () => {
expect(fetchMock).toHaveBeenCalledTimes(2);
expect(console.error).toHaveBeenCalled();
});
});
});

View file

@ -52,4 +52,4 @@ describe('checkStatus', () => {
expect(handleRequestError).toHaveBeenCalledTimes(1);
expect(fetch).not.toHaveBeenCalled();
});
});
});

View file

@ -74,4 +74,4 @@ describe('apiClient.makeRequest', () => {
expect(result).toBe(false);
expect(window.location.pathname).not.toBe(HTML_PAGES.LOGIN);
});
});
});

View file

@ -68,4 +68,4 @@ export const fetchRequest = async (url: string, method = 'GET', config: RequestC
data: responseData,
status: response.status,
};
};
};

View file

@ -39,4 +39,4 @@ export const buildChartData = (
index,
value,
label: formatHistoryLabel(index, interval, timeUnits, now),
}));
}));

View file

@ -257,17 +257,19 @@ func (s *StatsCtx) Close() (err error) {
err = errors.WithDeferred(err, cerr)
}()
// NOTE: This mutex, when combined with the database transaction, is
// required to be locked first.
s.currMu.RLock()
defer s.currMu.RUnlock()
udb := s.curr.serialize()
tx, err := db.Begin(true)
if err != nil {
return fmt.Errorf("opening transaction: %w", err)
}
defer func() { err = errors.WithDeferred(err, finishTxn(tx, err == nil)) }()
s.currMu.RLock()
defer s.currMu.RUnlock()
udb := s.curr.serialize()
return s.flushUnitToDB(udb, tx, s.curr.id)
}
@ -421,6 +423,8 @@ func (s *StatsCtx) flush() (cont bool, sleepFor time.Duration) {
s.confMu.Lock()
defer s.confMu.Unlock()
// NOTE: This mutex, when combined with the database transaction, is
// required to be locked first.
s.currMu.Lock()
defer s.currMu.Unlock()
@ -571,6 +575,11 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
return nil, 0
}
// NOTE: This mutex, when combined with the database transaction, is
// required to be locked first.
s.currMu.RLock()
defer s.currMu.RUnlock()
// Use writable transaction to ensure any ongoing writable transaction is
// taken into account.
tx, err := db.Begin(true)
@ -579,10 +588,6 @@ func (s *StatsCtx) loadUnits(limit uint32) (units []*unitDB, curID uint32) {
return nil, 0
}
s.currMu.RLock()
defer s.currMu.RUnlock()
cur := s.curr
if cur != nil {