Cherry-pick Go 1.25.8 upgrade for release-10.11 (#36125)

* ci: shard server Postgres tests into 4 parallel runners (#35739)

* ci: add test sharding plumbing to server CI

Add infrastructure for upcoming test sharding without changing behavior:

- Add shard-index and shard-total inputs to server-test-template.yml
  (defaults preserve existing single-runner behavior)
- Add timing cache restore step (activates only when shard-total > 1)
- Add merge-postgres-test-results job to server-ci.yml that:
  - Merges JUnit XML reports from shard artifacts
  - Saves timing data cache for future shard balancing
  - Handles both single-artifact and multi-shard scenarios
- Add .gitignore entries for timing cache and shard work files

Co-authored-by: Claude <claude@anthropic.com>

* ci: shard server Postgres tests into 4 parallel runners

Extract sharding logic into standalone, tested scripts and enable
4-shard parallel test execution for server Postgres CI:

Scripts:
- server/scripts/shard-split.js: Node.js bin-packing solver that
  assigns test packages to shards using timing data from previous runs.
  Two-tier strategy: light packages (<2min) whole, heavy packages
  (api4, app) split at individual test level.
- server/scripts/run-shard-tests.sh: Multi-run wrapper that calls
  gotestsum directly for each package group with -run regex filters.
- server/scripts/shard-split.test.js: 8 test cases covering round-robin
  fallback, timing-based balancing, heavy package splitting, JUnit XML
  fallback, and enterprise package separation.

Workflow changes:
- server-test-template.yml: Add shard splitting step that discovers test
  packages and runs the solver. Modified Run Tests step to use wrapper
  script when sharding is active.
- server-ci.yml: Add 4-shard matrix to test-postgres-normal. Update
  merge job artifact patterns for shard-specific names.

Performance: 7.2 min with timing cache vs 62.5 min baseline = 88%
wall-time improvement. First run without cache uses JUnit XML fallback
or round-robin, then populates the cache for subsequent runs.

Co-authored-by: Claude <claude@anthropic.com>

* fix: raise heavy package threshold to 5 min to preserve test isolation

sqlstore integrity tests scan the entire database and fail when other
packages' test data is present. At 182s, sqlstore was just over the
120s threshold and getting split at test level. Raising to 300s keeps
only api4 (~38 min) and app (~15 min) as heavy — where the real
sharding gains are — while sqlstore, elasticsearch, etc. stay whole
and maintain their test isolation guarantees.

Co-authored-by: Claude <claude@anthropic.com>

* ci: only save test timing cache on default branch

PR branches always restore from master's timing cache via restore-keys
prefix matching. Timing data is stable day-to-day so this eliminates
cache misses on first PR runs and reduces cache storage.

Co-authored-by: Claude <claude@anthropic.com>

* ci: skip FIPS tests on PRs (enterprise CI handles compile check)

Per review feedback: the enterprise CI already runs a FIPS compile
check on every PR. Running the full FIPS test suite on PRs is redundant
since it uses the identical test suite as non-FIPS — the only
FIPS-specific failure mode is a build failure from non-approved crypto
imports, which the enterprise compile check catches.

Full FIPS tests continue to run on every push to master.

Co-authored-by: Claude <claude@anthropic.com>

* fix: address review feedback on run-shard-tests.sh

- Remove set -e so all test runs execute even if earlier ones fail;
  track failures and exit with error at the end (wiggin77)
- Remove unused top-level COVERAGE_FLAG variable (wiggin77)
- Fix RUN_IDX increment position so report, json, and coverage files
  share the same index (wiggin77)
- Update workflow comment: heavy threshold is 5 min, not 2 min (wiggin77)

Co-authored-by: Claude <claude@anthropic.com>

* style: use node: prefix for built-in fs module in shard-split.js

Co-authored-by: Claude <claude@anthropic.com>

* fix: avoid interpolating file paths into generated shell script

Read shard package lists from files at runtime instead of interpolating
them into the generated script via printf. This prevents theoretical
shell metacharacter injection from directory names, as flagged by
DryRun Security.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): rename merged artifact to match server-ci-report glob

The merged artifact was named postgres-server-test-logs-merged which
does not match the *-test-logs pattern in server-ci-report.yml,
causing Postgres test results to be missing from PR/commit reports.

Also pins junit-report-merger to exact version 7.0.0 for supply chain
safety.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): pass RACE_MODE env into Docker container

RACE_MODE was set on the host runner but never included in the docker
run --env list. The light-package path worked because the heredoc
expanded on the host, but run-shard-tests.sh reads RACE_MODE at
runtime inside the container where it was unset. This caused heavy
packages (api4, app) to silently lose -race detection.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): discover new tests in heavy packages not in timing cache

Tests not present in the timing cache (newly added or renamed) would
not appear in any shard -run regex, causing them to silently skip.
After building items from the cache, run go test -list to discover
current test names and assign any cache-missing tests to shards via
the normal bin-packing algorithm with a small default duration.

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): add missing line continuation backslash in docker run

The previous --env FIPS_ENABLED line was missing a trailing backslash
after adding --env RACE_MODE, causing docker run to see a truncated
command and fail with "requires at least 1 argument".

Co-authored-by: Claude <claude@anthropic.com>

* fix(ci): add setup-go step for shard test discovery

go test -list in shard-split.js runs on the host runner via execSync,
but Go is only available inside the Docker container. Without this
step, every invocation fails silently and new-test discovery is a
no-op. Adding actions/setup-go before the shard split step ensures
the Go toolchain is available on the host.

Co-authored-by: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>

* updated go to version 1.25.8 (#35817)

* updated go to version 1.25.8

* updated gotestsum version to work with go 1.25.8

go 1.25 does not work with indirect tools 0.11 dependency pulled by
gotestsum.

* Use sync.WaitGroup.Go to simplify goroutine creation

Replace the wg.Add(1) + go func() { defer wg.Done() }() pattern with
wg.Go(), which was introduced in Go 1.25.

* pushes fips image on workflow dispatch to allow fips test to run on go version update

* fix new requirements for FIPS compliance imposed on updating to go 1.25.8

* updates openssl symbol check for library shipped with FIPS new versions

go-openssl v2 shipped with FIPS versions starting from 1.25 uses mkcgo to generate
bindings causing symbol names to be different.

* removes temp workflow-dispatch condition

* keep versions out of agents md file

* upgrade golangci-lint (#35845)

* test: clean up channel store data after TestChannelStore (#36066)

TestChannelStore sub-tests create channels, members, and team members
using fake TeamIds and UserIds (model.NewId() for non-existent rows).
These records are left in the database and cause integrity tests
(TestCheck*) running in the same binary to fail their full-table scans.

Register a t.Cleanup on TestChannelStore that purges the affected
tables entirely. A blanket purge is safe: the schema enforces no FK
constraints, and every test suite creates its own data independently.

* Fix command injection in server-test-template workflow (#36080)

Replace the unquoted heredoc (which embedded GITHUB_HEAD_REF into a
generated script) with a cp of the existing run-shard-tests.sh, which
already handles the light-only case. Pass BUILD_NUMBER and TEST_TARGET
as explicit docker env vars instead of interpolating them into script
content.

* fix(ci): restore testname format in sharded gotestsum runs (#36078)

run-shard-tests.sh called gotestsum directly without --format, so it
fell back to gotestsum's default (pkgname) instead of the testname
format set by the Makefile. Pass --format "${GOTESTSUM_FORMAT:-testname}"
to match the Makefile default.

Co-authored-by: Mattermost Build <build@mattermost.com>

* fix(lint): fix pre-existing golangci-lint v2.11.4 issues

Fix misspelling in comment and redundant nil check flagged by the
upgraded linter.

* ci: use golang image for test runner on release-10.11

mattermost-build-server images are not built for release branches.
Use the official golang image which is always available for any Go version.

* ci: use mattermost/mattermost-build-server for release-10.11

The mattermostdevelopment/ images are only built for master.
The production mattermost/ images are built for release branches.

* ci: use mattermost/mattermost-build-server in mmctl test template

The mattermostdevelopment/ images are only built for master.
The production mattermost/ images are built for release branches.

---------

Co-authored-by: Pavel Zeman <pavel.zeman@mattermost.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Carlos Garcia <carlos.garcia@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Jesse Hallam
2026-04-16 09:14:15 -03:00
коммит произвёл GitHub
родитель 384635216f
Коммит 73d5f507a4
37 изменённых файлов: 1053 добавлений и 257 удалений

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

@@ -65,9 +65,9 @@ func TestUnitUpdateConfig(t *testing.T) {
require.False(t, th.App.IsConfigReadOnly())
var called int32
var called atomic.Int32
th.App.AddConfigListener(func(old, current *model.Config) {
atomic.AddInt32(&called, 1)
called.Add(1)
assert.Equal(t, prev, *old.ServiceSettings.SiteURL)
assert.Equal(t, "http://foo.com", *current.ServiceSettings.SiteURL)
})
@@ -77,7 +77,7 @@ func TestUnitUpdateConfig(t *testing.T) {
})
// callback should be called once
assert.Equal(t, int32(1), atomic.LoadInt32(&called))
assert.Equal(t, int32(1), called.Load())
}
func TestDoAdvancedPermissionsMigration(t *testing.T) {

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

@@ -129,9 +129,9 @@ func TestCheckPendingNotifications(t *testing.T) {
}})
require.NoError(t, nErr)
var wasCalled int32
var wasCalled atomic.Int32
job.checkPendingNotifications(time.Unix(10050, 0), func(string, []*batchedNotification) {
atomic.StoreInt32(&wasCalled, int32(1))
wasCalled.Store(int32(1))
})
// A hack to check whether the handler was called.
@@ -141,7 +141,7 @@ func TestCheckPendingNotifications(t *testing.T) {
// We do a check outside the email handler, because otherwise, failing from
// inside the handler doesn't let the .Go() function exit cleanly, and it gets
// stuck during server shutdown, trying to wait for the goroutine to exit
require.Equal(t, int32(0), atomic.LoadInt32(&wasCalled), "email handler should not have been called")
require.Equal(t, int32(0), wasCalled.Load(), "email handler should not have been called")
require.Nil(t, job.pendingNotifications[th.BasicUser.Id])
require.Empty(t, job.pendingNotifications[th.BasicUser.Id], "should've remove queued post since user acted")

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

@@ -186,12 +186,9 @@ func TestIsFirstUserAccountThunderingHerd(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < te.concurrentRequest; i++ {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
require.Equal(t, te.result, th.Service.IsFirstUserAccount())
}()
})
}
wg.Wait()

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

@@ -407,11 +407,9 @@ func (wc *WebConn) SetSession(v *model.Session) {
// is ready to send/receive messages.
func (wc *WebConn) Pump() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
wc.writePump()
}()
})
wg.Add(1)
go wc.pluginPostedConsumer(&wg)

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

@@ -520,7 +520,7 @@ func TestPreparePostForClient(t *testing.T) {
name: "markdown",
link: "[markdown](%s) link",
// This is because markdown links are not currently supported in the opengraph fetching code
// if you just implmented this, remove the `notImplmented` field
// if you just implemented this, remove the `notImplmented` field
notImplmented: true,
},
{

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

@@ -162,9 +162,7 @@ func TestCreatePostDeduplicate(t *testing.T) {
// Launch a goroutine to make the first CreatePost call that will get delayed
// by the plugin above.
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
var appErr *model.AppError
post, _, appErr = th.App.CreatePostAsUser(th.Context.WithSession(session), &model.Post{
UserId: th.BasicUser.Id,
@@ -174,7 +172,7 @@ func TestCreatePostDeduplicate(t *testing.T) {
}, session.Id, true)
require.Nil(t, appErr)
require.Equal(t, post.Message, "plugin delayed")
}()
})
// Give the goroutine above a chance to start and get delayed by the plugin.
time.Sleep(2 * time.Second)
@@ -3263,12 +3261,10 @@ func TestCollapsedThreadFetch(t *testing.T) {
// we introduce a race to trigger an unexpected error from the db side.
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
err := th.Server.Store().Post().PermanentDeleteByUser(th.Context, user1.Id)
require.NoError(t, err)
}()
})
require.NotPanics(t, func() {
// We're only testing that this doesn't panic, not checking the error

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

@@ -453,7 +453,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// - Cursor updates only when sync is successful
EnsureCleanState(t, th, ss)
var syncAttempts int32
var syncAttempts atomic.Int32
var failureMode atomic.Bool
failureMode.Store(false)
var syncHandler *SelfReferentialSyncHandler
@@ -462,7 +462,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v4/remotecluster/msg":
atomic.AddInt32(&syncAttempts, 1)
syncAttempts.Add(1)
if failureMode.Load() {
w.WriteHeader(http.StatusInternalServerError)
@@ -520,7 +520,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Wait for first sync
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) > 0
return syncAttempts.Load() > 0
}, 5*time.Second, 100*time.Millisecond, "Should have attempted sync")
// Verify cursor was updated
@@ -539,13 +539,13 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
require.NoError(t, err)
// Second sync - should fail
initialAttempts := atomic.LoadInt32(&syncAttempts)
initialAttempts := syncAttempts.Load()
err = service.HandleSyncAllUsersForTesting(selfCluster)
require.NoError(t, err) // The method itself shouldn't error, just the remote call
// Wait for failed sync attempt
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) > initialAttempts
return syncAttempts.Load() > initialAttempts
}, 5*time.Second, 100*time.Millisecond, "Should have attempted sync")
// Verify cursor was NOT updated on failure
@@ -557,13 +557,13 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
failureMode.Store(false)
// Third sync - should succeed and update cursor
preSuccessAttempts := atomic.LoadInt32(&syncAttempts)
preSuccessAttempts := syncAttempts.Load()
err = service.HandleSyncAllUsersForTesting(selfCluster)
require.NoError(t, err)
// Wait for successful sync
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) > preSuccessAttempts
return syncAttempts.Load() > preSuccessAttempts
}, 5*time.Second, 100*time.Millisecond, "Should have attempted sync")
// Verify cursor was updated after successful sync
@@ -579,12 +579,12 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// - Ensures cursor is only updated when flag is enabled
EnsureCleanState(t, th, ss)
var syncMessageCount int32
var syncMessageCount atomic.Int32
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
atomic.AddInt32(&syncMessageCount, 1)
syncMessageCount.Add(1)
}
writeOKResponse(w)
}))
@@ -622,13 +622,13 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
err = th.App.ReloadConfig()
require.NoError(t, err)
atomic.StoreInt32(&syncMessageCount, 0)
syncMessageCount.Store(0)
err = service.HandleSyncAllUsersForTesting(selfCluster)
require.NoError(t, err)
// Verify no sync messages were sent
require.Never(t, func() bool {
return atomic.LoadInt32(&syncMessageCount) > 0
return syncMessageCount.Load() > 0
}, 2*time.Second, 100*time.Millisecond, "No sync should occur with feature flag disabled")
// Verify cursor was not updated
@@ -645,13 +645,13 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
err = th.App.ReloadConfig()
require.NoError(t, err)
atomic.StoreInt32(&syncMessageCount, 0)
syncMessageCount.Store(0)
err = service.HandleSyncAllUsersForTesting(selfCluster)
require.NoError(t, err)
// Verify sync messages were sent
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncMessageCount) > 0
return syncMessageCount.Load() > 0
}, 5*time.Second, 100*time.Millisecond, "Sync should occur with feature flag enabled")
// Verify cursor was updated
@@ -667,13 +667,13 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// - Tests cursor updates in both scenarios
EnsureCleanState(t, th, ss)
var syncMessageCount int32
var syncMessageCount atomic.Int32
var connectionOpenSyncOccurred atomic.Bool
// Create test HTTP server
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
atomic.AddInt32(&syncMessageCount, 1)
syncMessageCount.Add(1)
// Parse message to check if it's a user sync
bodyBytes, _ := io.ReadAll(r.Body)
@@ -724,12 +724,12 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Verify no automatic sync occurs within a reasonable time
require.Never(t, func() bool {
return connectionOpenSyncOccurred.Load() || atomic.LoadInt32(&syncMessageCount) > 0
return connectionOpenSyncOccurred.Load() || syncMessageCount.Load() > 0
}, 2*time.Second, 100*time.Millisecond, "No automatic sync should occur when config is disabled")
// Test 2: Connection open with sync enabled
// Reset counters
atomic.StoreInt32(&syncMessageCount, 0)
syncMessageCount.Store(0)
connectionOpenSyncOccurred.Store(false)
// Enable config option
@@ -767,7 +767,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
}, 5*time.Second, 100*time.Millisecond, "Automatic sync should occur when config is enabled")
// Verify sync occurred
assert.Greater(t, atomic.LoadInt32(&syncMessageCount), int32(0), "Should have sync messages when config enabled")
assert.Greater(t, syncMessageCount.Load(), int32(0), "Should have sync messages when config enabled")
// Verify cursor was updated
updatedCluster, err2 := ss.RemoteCluster().Get(selfCluster2.RemoteId, true)
@@ -849,7 +849,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// - No partial data should be persisted
EnsureCleanState(t, th, ss)
var syncAttempts int32
var syncAttempts atomic.Int32
var serverOnline atomic.Bool
serverOnline.Store(true)
@@ -862,9 +862,9 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
}
if r.URL.Path == "/api/v4/remotecluster/msg" {
atomic.AddInt32(&syncAttempts, 1)
syncAttempts.Add(1)
// On second attempt, go offline
if atomic.LoadInt32(&syncAttempts) >= 2 {
if syncAttempts.Load() >= 2 {
serverOnline.Store(false)
w.WriteHeader(http.StatusServiceUnavailable)
return
@@ -903,7 +903,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Wait for first sync
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) >= 1
return syncAttempts.Load() >= 1
}, 5*time.Second, 100*time.Millisecond)
// Get cursor after first sync
@@ -926,7 +926,7 @@ func TestSharedChannelGlobalUserSyncSelfReferential(t *testing.T) {
// Wait for second sync attempt
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) >= 2
return syncAttempts.Load() >= 2
}, 5*time.Second, 100*time.Millisecond)
// Verify cursor was not updated after failed sync

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

@@ -66,12 +66,12 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// The test ensures that sync messages are sent asynchronously after a minimum delay for both add and remove operations.
EnsureCleanState(t, th, ss)
// Track sync messages received
var syncMessageCount int32
var syncMessageCount atomic.Int32
var syncHandler *SelfReferentialSyncHandler
// Create a test HTTP server that acts as the "remote" cluster
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
atomic.AddInt32(&syncMessageCount, 1)
syncMessageCount.Add(1)
if syncHandler != nil {
syncHandler.HandleRequest(w, r)
} else {
@@ -145,7 +145,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for async sync with more generous timeout (minimum delay is 2 seconds + async task processing)
require.Eventually(t, func() bool {
count := atomic.LoadInt32(&syncMessageCount)
count := syncMessageCount.Load()
return count > 0
}, 15*time.Second, 200*time.Millisecond, "Should have received at least one sync message via automatic sync")
@@ -162,7 +162,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Reset sync counter and wait for background tasks to settle
var initialCount int32
require.Eventually(t, func() bool {
initialCount = atomic.LoadInt32(&syncMessageCount)
initialCount = syncMessageCount.Load()
return !service.HasPendingTasksForTesting()
}, 5*time.Second, 100*time.Millisecond, "Background tasks should settle before removal test")
@@ -172,7 +172,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for removal sync with increased timeout
require.Eventually(t, func() bool {
count := atomic.LoadInt32(&syncMessageCount)
count := syncMessageCount.Load()
return count > initialCount
}, 20*time.Second, 200*time.Millisecond, "Should have received sync message for user removal")
@@ -534,7 +534,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// 2. No members are synced during failure mode
// 3. Once the server recovers, sync completes successfully
EnsureCleanState(t, th, ss)
var syncAttempts int32
var syncAttempts atomic.Int32
var failureMode atomic.Bool
failureMode.Store(true)
var successfulSyncs []string
@@ -543,7 +543,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
atomic.AddInt32(&syncAttempts, 1)
syncAttempts.Add(1)
if failureMode.Load() {
w.WriteHeader(http.StatusInternalServerError)
@@ -624,11 +624,11 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for first sync attempt with more robust checking
require.Eventually(t, func() bool {
attempts := atomic.LoadInt32(&syncAttempts)
attempts := syncAttempts.Load()
return attempts > 0
}, 15*time.Second, 100*time.Millisecond, "Should have attempted sync during failure mode")
initialAttempts := atomic.LoadInt32(&syncAttempts)
initialAttempts := syncAttempts.Load()
assert.Greater(t, initialAttempts, int32(0), "Should have attempted sync")
assert.Empty(t, successfulSyncs, "No successful syncs during failure mode")
@@ -650,7 +650,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
}, 15*time.Second, 100*time.Millisecond, "Should have successful sync after recovery")
// Verify recovery
finalAttempts := atomic.LoadInt32(&syncAttempts)
finalAttempts := syncAttempts.Load()
assert.Greater(t, finalAttempts, initialAttempts, "Should have retried after recovery")
})
t.Run("Test 5: Manual sync with cursor management", func(t *testing.T) {
@@ -660,9 +660,9 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// 3. Verifies all operations are properly synced and cursor is updated correctly
// 4. Validates that the LastMembersSyncAt cursor advances after each sync operation
EnsureCleanState(t, th, ss)
var totalSyncMessages int32
var addOperations int32
var removeOperations int32
var totalSyncMessages atomic.Int32
var addOperations atomic.Int32
var removeOperations atomic.Int32
var selfCluster *model.RemoteCluster
// Create sync handler
@@ -686,9 +686,9 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Count membership changes from the unified field
for _, change := range syncMsg.MembershipChanges {
if change.IsAdd {
atomic.AddInt32(&addOperations, 1)
addOperations.Add(1)
} else {
atomic.AddInt32(&removeOperations, 1)
removeOperations.Add(1)
}
}
}
@@ -773,10 +773,10 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for initial sync to complete
require.Eventually(t, func() bool {
return atomic.LoadInt32(&addOperations) >= 10
return addOperations.Load() >= 10
}, 10*time.Second, 100*time.Millisecond, "Should sync all initial users")
initialAdds := atomic.LoadInt32(&addOperations)
initialAdds := addOperations.Load()
assert.GreaterOrEqual(t, initialAdds, int32(10), "Should sync all initial users")
// Verify cursor was updated after initial sync
@@ -808,15 +808,15 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
}
// Sync mixed changes
previousMessages := atomic.LoadInt32(&totalSyncMessages)
previousMessages := totalSyncMessages.Load()
err = service.SyncAllChannelMembers(channel.Id, selfCluster.RemoteId, nil)
require.NoError(t, err)
// Wait for mixed changes sync to complete
require.Eventually(t, func() bool {
messages := atomic.LoadInt32(&totalSyncMessages)
removes := atomic.LoadInt32(&removeOperations)
messages := totalSyncMessages.Load()
removes := removeOperations.Load()
return messages > previousMessages && removes >= 3
}, 10*time.Second, 100*time.Millisecond, "Should sync mixed changes")
@@ -836,9 +836,9 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
expectedMembers := 10 - 3 + 5 + 1 // initial - removed + added + system admin
assert.Equal(t, expectedMembers, len(members), "Should have correct final member count")
finalMessages := atomic.LoadInt32(&totalSyncMessages)
finalAdds := atomic.LoadInt32(&addOperations)
finalRemoves := atomic.LoadInt32(&removeOperations)
finalMessages := totalSyncMessages.Load()
finalAdds := addOperations.Load()
finalRemoves := removeOperations.Load()
assert.Greater(t, finalMessages, int32(0), "Should have sync messages")
assert.Greater(t, finalAdds, int32(0), "Should have add operations")
@@ -850,7 +850,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// 2. Changes from one cluster propagate through our server to other clusters
// 3. Removals sync to all clusters
EnsureCleanState(t, th, ss)
var totalSyncMessages int32
var totalSyncMessages atomic.Int32
var syncMessagesPerCluster = make(map[string]*int32)
// Create multiple test HTTP servers to simulate different remote clusters
@@ -997,7 +997,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// This simulates cluster-2 receiving a membership change and propagating it
// Reset counters
atomic.StoreInt32(&totalSyncMessages, 0)
totalSyncMessages.Store(0)
for _, countPtr := range syncMessagesPerCluster {
atomic.StoreInt32(countPtr, 0)
}
@@ -1058,7 +1058,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Part 3: Test removal syncing to all clusters
// Reset counters
atomic.StoreInt32(&totalSyncMessages, 0)
totalSyncMessages.Store(0)
for _, countPtr := range syncMessagesPerCluster {
atomic.StoreInt32(countPtr, 0)
}
@@ -1098,7 +1098,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// 2. When the feature flag is enabled, sync messages should be sent as expected
// This ensures that the feature can be safely disabled in production without triggering unintended syncs
EnsureCleanState(t, th, ss)
var syncMessageCount int32
var syncMessageCount atomic.Int32
// Disable feature flag from the beginning to prevent any automatic sync
os.Setenv("MM_FEATUREFLAGS_ENABLESHAREDCHANNELMEMBERSYNC", "false")
@@ -1108,7 +1108,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Create test HTTP server that counts sync messages
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v4/remotecluster/msg" {
atomic.AddInt32(&syncMessageCount, 1)
syncMessageCount.Add(1)
}
writeOKResponse(w)
}))
@@ -1168,13 +1168,13 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
require.Nil(t, appErr)
}
atomic.StoreInt32(&syncMessageCount, 0)
syncMessageCount.Store(0)
err = service.SyncAllChannelMembers(channel.Id, selfCluster.RemoteId, nil)
require.NoError(t, err)
// Verify no sync messages were sent
require.Never(t, func() bool {
return atomic.LoadInt32(&syncMessageCount) > 0
return syncMessageCount.Load() > 0
}, 2*time.Second, 100*time.Millisecond, "No sync should occur with feature flag disabled")
// Test 2: Sync with feature flag enabled
@@ -1182,13 +1182,13 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
cfg.FeatureFlags.EnableSharedChannelsMemberSync = true
})
atomic.StoreInt32(&syncMessageCount, 0)
syncMessageCount.Store(0)
err = service.SyncAllChannelMembers(channel.Id, selfCluster.RemoteId, nil)
require.NoError(t, err)
// Verify sync messages were sent
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncMessageCount) > 0
return syncMessageCount.Load() > 0
}, 5*time.Second, 100*time.Millisecond, "Sync should occur with feature flag enabled")
})
t.Run("Test 8: Sync Task After Connection Becomes Available", func(t *testing.T) {
@@ -1307,7 +1307,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// 5. No partial data is persisted from the failed sync
EnsureCleanState(t, th, ss)
var syncAttempts int32
var syncAttempts atomic.Int32
var serverOnline atomic.Bool
serverOnline.Store(true)
var syncHandler *SelfReferentialSyncHandler
@@ -1321,7 +1321,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
}
if r.URL.Path == "/api/v4/remotecluster/msg" {
currentAttempt := atomic.AddInt32(&syncAttempts, 1)
currentAttempt := syncAttempts.Add(1)
// On second sync cycle, go offline (allow first full sync to complete)
if currentAttempt > 2 {
serverOnline.Store(false)
@@ -1398,7 +1398,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for first sync with more generous timeout
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) >= 1
return syncAttempts.Load() >= 1
}, 15*time.Second, 200*time.Millisecond, "Should complete first sync")
// Wait for cursor to be updated after first sync
@@ -1427,7 +1427,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for second sync attempt with more generous timeout
require.Eventually(t, func() bool {
return atomic.LoadInt32(&syncAttempts) >= 2
return syncAttempts.Load() >= 2
}, 20*time.Second, 200*time.Millisecond, "Should attempt second sync")
// Wait for any cursor updates to complete and verify cursor was not updated
@@ -1457,7 +1457,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
var mu sync.Mutex
var syncHandler *SelfReferentialSyncHandler
var testServer *httptest.Server
var totalSyncMessages int32
var totalSyncMessages atomic.Int32
// Create users
user1 := th.CreateUser()
@@ -1528,7 +1528,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
for _, change := range syncMsg.MembershipChanges {
if change.IsAdd {
syncedChannelUsers[channelId] = append(syncedChannelUsers[channelId], change.UserId)
atomic.AddInt32(&totalSyncMessages, 1)
totalSyncMessages.Add(1)
}
}
}
@@ -1583,7 +1583,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Ensure the sync handler is ready by waiting for the first message
require.Eventually(t, func() bool {
return atomic.LoadInt32(&totalSyncMessages) > 0
return totalSyncMessages.Load() > 0
}, 10*time.Second, 50*time.Millisecond, "Expected at least one sync message to be sent")
// Calculate expected number of sync messages
@@ -1595,7 +1595,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// Wait for all sync messages to be processed with detailed debugging
require.Eventually(t, func() bool {
currentMessages := atomic.LoadInt32(&totalSyncMessages)
currentMessages := totalSyncMessages.Load()
mu.Lock()
channelCount := len(syncedChannelUsers)
@@ -1613,7 +1613,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
return currentMessages >= expectedSyncMessages
}, 30*time.Second, 200*time.Millisecond,
fmt.Sprintf("Expected %d sync messages, but got %d", expectedSyncMessages, atomic.LoadInt32(&totalSyncMessages)))
fmt.Sprintf("Expected %d sync messages, but got %d", expectedSyncMessages, totalSyncMessages.Load()))
// Verify we have complete data for all channels
require.Eventually(t, func() bool {
@@ -1697,7 +1697,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// EnsureCleanState(t, th, ss)
// var syncMessages []model.SyncMsg
// var mu sync.Mutex
// var syncMessageCount int32
// var syncMessageCount atomic.Int32
// var selfCluster *model.RemoteCluster
// // Create sync handler
@@ -1706,7 +1706,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Create test HTTP server that tracks sync messages
// testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// if r.URL.Path == "/api/v4/remotecluster/msg" {
// atomic.AddInt32(&syncMessageCount, 1)
// syncMessageCount.Add(1)
// // Read body once
// bodyBytes, readErr := io.ReadAll(r.Body)
@@ -1806,7 +1806,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Wait for initial sync to complete
// require.Eventually(t, func() bool {
// count := atomic.LoadInt32(&syncMessageCount)
// count := syncMessageCount.Load()
// return count > 0
// }, 15*time.Second, 200*time.Millisecond, "Should have initial sync messages")
@@ -1853,7 +1853,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Phase 5: Conflict resolution sync
// // Reset message tracking for conflict resolution phase
// atomic.StoreInt32(&syncMessageCount, 0)
// syncMessageCount.Store(0)
// mu.Lock()
// syncMessages = []model.SyncMsg{}
// mu.Unlock()
@@ -1863,7 +1863,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Wait for conflict resolution sync to complete
// require.Eventually(t, func() bool {
// count := atomic.LoadInt32(&syncMessageCount)
// count := syncMessageCount.Load()
// return count > 0
// }, 20*time.Second, 200*time.Millisecond, "Should receive conflict resolution sync messages")
@@ -1907,7 +1907,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// require.Nil(t, appErr)
// // Reset and sync this new user
// atomic.StoreInt32(&syncMessageCount, 0)
// syncMessageCount.Store(0)
// mu.Lock()
// syncMessages = []model.SyncMsg{}
// mu.Unlock()
@@ -1932,7 +1932,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// }, 15*time.Second, 200*time.Millisecond, "New user should be synced correctly after conflict resolution")
// // Phase 9: Verify efficiency - no redundant syncs for existing members
// atomic.StoreInt32(&syncMessageCount, 0)
// syncMessageCount.Store(0)
// mu.Lock()
// syncMessages = []model.SyncMsg{}
// mu.Unlock()
@@ -1944,7 +1944,7 @@ func TestSharedChannelMembershipSyncSelfReferential(t *testing.T) {
// // Wait for sync completion and verify minimal activity
// // Give time for any sync to complete, then check the final count
// require.Eventually(t, func() bool {
// finalCount := atomic.LoadInt32(&syncMessageCount)
// finalCount := syncMessageCount.Load()
// // Should have minimal activity since all members are already synced
// return finalCount <= 1
// }, 10*time.Second, 200*time.Millisecond, "Should have minimal sync activity for already-synced members")

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

@@ -41,7 +41,7 @@ type SelfReferentialSyncHandler struct {
t *testing.T
service *sharedchannel.Service
selfCluster *model.RemoteCluster
syncMessageCount *int32
syncMessageCount *atomic.Int32
SimulateUnshared bool // When true, always return ErrChannelIsNotShared for sync messages
// Callbacks for capturing sync data
@@ -52,12 +52,11 @@ type SelfReferentialSyncHandler struct {
// NewSelfReferentialSyncHandler creates a new handler for processing sync messages in tests
func NewSelfReferentialSyncHandler(t *testing.T, service *sharedchannel.Service, selfCluster *model.RemoteCluster) *SelfReferentialSyncHandler {
count := int32(0)
return &SelfReferentialSyncHandler{
t: t,
service: service,
selfCluster: selfCluster,
syncMessageCount: &count,
syncMessageCount: &atomic.Int32{},
}
}
@@ -69,7 +68,7 @@ func NewSelfReferentialSyncHandler(t *testing.T, service *sharedchannel.Service,
func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v4/remotecluster/msg":
currentCall := atomic.AddInt32(h.syncMessageCount, 1)
currentCall := h.syncMessageCount.Add(1)
// Read and process the sync message
body, _ := io.ReadAll(r.Body)
@@ -162,7 +161,7 @@ func (h *SelfReferentialSyncHandler) HandleRequest(w http.ResponseWriter, r *htt
// GetSyncMessageCount returns the current count of sync messages received
func (h *SelfReferentialSyncHandler) GetSyncMessageCount() int32 {
return atomic.LoadInt32(h.syncMessageCount)
return h.syncMessageCount.Load()
}
// EnsureCleanState ensures a clean test state by removing all shared channels, remote clusters,

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

@@ -38,10 +38,7 @@ func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPack
mut sync.Mutex // Protects warnings and fileDatas
)
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
for name, fn := range functions {
fileData, err := fn(rctx)
mut.Lock()
@@ -82,14 +79,11 @@ func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPack
}
}
mut.Unlock()
}()
})
// Run the cluster generation in a separate goroutine as CPU profile generation and file upload can take a long time
if cluster := a.Cluster(); cluster != nil && *a.Config().ClusterSettings.Enable {
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
files, err := cluster.GenerateSupportPacket(rctx, options)
mut.Lock()
if err != nil {
@@ -101,7 +95,7 @@ func (a *App) GenerateSupportPacket(rctx request.CTX, options *model.SupportPack
fileDatas = append(fileDatas, node...)
}
mut.Unlock()
}()
})
}
wg.Wait()

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

@@ -116,9 +116,8 @@ var versionPrefixes = []string{
func getBrowserVersion(ua *uasurfer.UserAgent, userAgentString string) string {
for _, prefix := range versionPrefixes {
if index := strings.Index(userAgentString, prefix); index != -1 {
afterPrefix := userAgentString[index+len(prefix):]
if fields := strings.Fields(afterPrefix); len(fields) > 0 {
if _, after, ok := strings.Cut(userAgentString, prefix); ok {
if fields := strings.Fields(after); len(fields) > 0 {
// MM-55320: limitStringLength prevents potential DOS caused by filling an unbounded string with junk data
return limitStringLength(fields[0], maxUserAgentVersionLength)
}

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

@@ -28,7 +28,7 @@ type Worker struct {
jobServer *jobs.JobServer
logger mlog.LoggerIFace
store store.Store
closed int32
closed atomic.Int32
}
func MakeWorker(jobServer *jobs.JobServer, store store.Store) *Worker {
@@ -48,7 +48,7 @@ func MakeWorker(jobServer *jobs.JobServer, store store.Store) *Worker {
func (worker *Worker) Run() {
// Set to open if closed before. We are not bothered about multiple opens.
if atomic.CompareAndSwapInt32(&worker.closed, 1, 0) {
if worker.closed.CompareAndSwap(1, 0) {
worker.stop = make(chan struct{})
}
worker.logger.Debug("Worker started")
@@ -71,7 +71,7 @@ func (worker *Worker) Run() {
func (worker *Worker) Stop() {
// Set to close, and if already closed before, then return.
if !atomic.CompareAndSwapInt32(&worker.closed, 0, 1) {
if !worker.closed.CompareAndSwap(0, 1) {
return
}
worker.logger.Debug("Worker stopping")

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

@@ -44,6 +44,20 @@ type SqlXExecutor interface {
Select(dest any, query string, args ...any) error
}
// cleanupChannelStoreData purges all channel-related data written by TestChannelStore
// sub-tests. The integrity tests (TestCheck*) do full-table scans and fail if any
// orphaned rows remain. A blanket purge is safe: no FK constraints are enforced in the
// schema, and every test suite creates its own data independently.
func cleanupChannelStoreData(t *testing.T, s SqlStore) {
t.Helper()
db := s.GetMaster()
db.Exec(`DELETE FROM Threads`)
db.Exec(`DELETE FROM ChannelMemberHistory`)
db.Exec(`DELETE FROM ChannelMembers`)
db.Exec(`DELETE FROM Channels`)
db.Exec(`DELETE FROM TeamMembers`)
}
func cleanupChannels(t *testing.T, rctx request.CTX, ss store.Store) {
list, err := ss.Channel().GetAllChannels(0, 100000, store.ChannelSearchOpts{IncludeDeleted: true})
require.NoError(t, err, "error cleaning all channels", err)
@@ -69,6 +83,7 @@ func channelMemberToJSON(t *testing.T, cm *model.ChannelMember) string {
func TestChannelStore(t *testing.T, rctx request.CTX, ss store.Store, s SqlStore) {
createDefaultRoles(ss)
t.Cleanup(func() { cleanupChannelStoreData(t, s) })
t.Run("Save", func(t *testing.T) { testChannelStoreSave(t, rctx, ss) })
t.Run("SaveDirectChannel", func(t *testing.T) { testChannelStoreSaveDirectChannel(t, rctx, ss, s) })

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

@@ -149,14 +149,10 @@ func testCreateInitialSidebarCategories(t *testing.T, rctx request.CTX, ss store
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for range 10 {
wg.Go(func() {
_, _ = ss.Channel().CreateInitialSidebarCategories(rctx, userID, team.Id)
}()
})
}
wg.Wait()
@@ -2298,15 +2294,13 @@ func doTestSidebarCategoryConcurrentAccess(t *testing.T, rctx request.CTX, ss st
// Run concurrent operations
var wg sync.WaitGroup
for i := 0; i < numGoroutines; i++ {
wg.Add(1)
for i := range numGoroutines {
// Run GetSidebarCategoriesForTeamForUser
go func() {
defer wg.Done()
wg.Go(func() {
categories, getErr := ss.Channel().GetSidebarCategoriesForTeamForUser(userID, team.Id)
require.NoError(t, getErr)
require.NotEmpty(t, categories.Categories)
}()
})
// Run UpdateSidebarCategories with different update patterns
wg.Add(1)

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

@@ -30,10 +30,11 @@ func TestIncomingWebhook(t *testing.T) {
url := apiClient.URL + "/hooks/" + hook.Id
tooLongText := ""
for i := 0; i < 8200; i++ {
tooLongText += "a"
var tooLongTextBuilder strings.Builder
for range 8200 {
tooLongTextBuilder.WriteString("a")
}
tooLongText := tooLongTextBuilder.String()
t.Run("WebhookBasics", func(t *testing.T) {
payload := "payload={\"text\": \"test text\"}"