[AI assisted] MM-64298: Process setting status offline in batches (#31065)

When a user disconnects from the hub, we would spawn off a goroutine
which would make a cluster request, and then update the user status
as offline in the DB.

This was another case of unbounded concurrency where the number of
goroutines spawned was user controlled. Therefore, we would see
a clear spike in DB connections on master when a lot of users
would suddenly disconnect.

To fix this, we implement concurrency control in two areas:
1. In making the cluster request. We implement a counting semaphore
per-hub to avoid making unbounded cluster requests.
2. We use a buffered channel with a periodic flusher to process
status updates.

We also add a new store method to upsert multiple statuses
in a single query. The statusUpdateThreshold is set to 32, which means
no more than 32 rows will be upserted at one time, keeping the
SQL query load reasonable.

https://mattermost.atlassian.net/browse/MM-64298

```release-note
We improve DB connection spikes on user disconnect
by processing status updates in batches.
```
Этот коммит содержится в:
Agniva De Sarker
2025-06-17 09:20:34 +05:30
коммит произвёл GitHub
родитель b99a22f175
Коммит 761bc7549b
11 изменённых файлов: 415 добавлений и 24 удалений

Просмотреть файл

@@ -50,6 +50,11 @@ type PlatformService struct {
filestore filestore.FileBackend
exportFilestore filestore.FileBackend
// Channel for batching status updates
statusUpdateChan chan *model.Status
statusUpdateExitSignal chan struct{}
statusUpdateDoneSignal chan struct{}
cacheProvider cache.Provider
statusCache cache.Cache
sessionCache cache.Cache
@@ -136,6 +141,9 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
},
licenseListeners: map[string]func(*model.License, *model.License){},
additionalClusterHandlers: map[model.ClusterEvent]einterfaces.ClusterMessageHandler{},
statusUpdateChan: make(chan *model.Status, statusUpdateBufferSize),
statusUpdateExitSignal: make(chan struct{}),
statusUpdateDoneSignal: make(chan struct{}),
}
// Assume the first user account has not been created yet. A call to the DB will later check if this is really the case.
@@ -397,6 +405,10 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
}
func (ps *PlatformService) Start(broadcastHooks map[string]BroadcastHook) error {
// Start the status update processor.
// Must be done before hub start.
go ps.processStatusUpdates()
ps.hubStart(broadcastHooks)
ps.configListenerId = ps.AddConfigListener(func(_, _ *model.Config) {
@@ -497,6 +509,12 @@ func (ps *PlatformService) TotalWebsocketConnections() int {
func (ps *PlatformService) Shutdown() error {
ps.HubStop()
// Shutdown status processor.
// Must be done after hub shutdown.
close(ps.statusUpdateExitSignal)
// wait for it to be stopped.
<-ps.statusUpdateDoneSignal
ps.RemoveLicenseListener(ps.licenseListenerId)
// we need to wait the goroutines to finish before closing the store