Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/5-internal/WPB-22954
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Migration of mls commit locks from cassandra to postgres
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ data:
migrateConversationCodes: {{ .migrateConversationCodes }}
migrateTeamFeatures: {{ .migrateTeamFeatures }}
migrateDomainRegistration: {{ .migrateDomainRegistration }}
migrateMLSCommitLocks: {{ .migrateMLSCommitLocks }}
migrationOptions:
{{ toYaml .migrationOptions | indent 6 }}

Expand Down
5 changes: 5 additions & 0 deletions charts/wire-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ galley:
teamFeatures: cassandra
domainRegistration: cassandra
user: cassandra
mlsCommitLocks: cassandra
settings:
httpPoolSize: 128
maxTeamSize: 10000
Expand Down Expand Up @@ -1017,6 +1018,10 @@ background-worker:
# It's important to set `settings.postgresMigration.domainRegistration` to `migration-to-postgresql`
# before starting the migration.
migrateDomainRegistration: false
# This will start the migration of mls commit locks.
# It's important to set `settings.postgresMigration.mlsCommitLocks` to `migration-to-postgresql`
# before starting the migration.
migrateMLSCommitLocks: false

backendNotificationPusher:
pushBackoffMinWait: 10000 # in microseconds, so 10ms
Expand Down
19 changes: 14 additions & 5 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2106,12 +2106,14 @@ galley:
teamFeatures: postgresql
domainRegistration: postgresql
user: postgresql
mlsCommitLocks: postgresql
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateMLSCommitLocks: false
```

#### Migration for existing installations
Expand Down Expand Up @@ -2142,6 +2144,7 @@ The current settings and their background-worker flags are:
- `conversationCodes` -> `migrateConversationCodes`
- `teamFeatures` -> `migrateTeamFeatures`
- `domainRegistration` -> `migrateDomainRegistration`
- `mlsCommitLocks` -> `migrateMLSCommitLocks`

**Migration pattern per migration setting**

Expand All @@ -2160,13 +2163,15 @@ The current settings and their background-worker flags are:
conversation: migration-to-postgresql
conversationCodes: migration-to-postgresql
teamFeatures: migration-to-postgresql
domainRegistration: cassandra
domainRegistration: cassandra
mlsCommitLocks: cassandra
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateDomainRegistration: false
migrateMLSCommitLocks: false
```

This change should restart the affected pods, and new writes will follow the
Expand All @@ -2180,7 +2185,8 @@ The current settings and their background-worker flags are:
migrateConversations: true
migrateConversationCodes: true
migrateTeamFeatures: true
migrateDomainRegistration: true
migrateDomainRegistration: true
migrateMLSCommitLocks: true
```

During migration, Cassandra rows are not deleted. Writes and migration share
Expand All @@ -2196,6 +2202,7 @@ The current settings and their background-worker flags are:
- `conversationCodes`: `wire_conv_codes_migration_finished`
- `teamFeatures`: `wire_team_features_migration_finished`
- `domainRegistration`: `wire_domain_registration_migration_finished`
- `mlsCommitLocks`: `wire_mls_commit_locks_migration_finished`

3. Cut over reads and writes to PostgreSQL for the selected migration
setting(s). This configuration must be used from now on for every new
Expand All @@ -2208,13 +2215,15 @@ The current settings and their background-worker flags are:
conversation: postgresql
conversationCodes: postgresql
teamFeatures: postgresql
domainRegistration: cassandra
domainRegistration: cassandra
mlsCommitLocks: cassandra
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateDomainRegistration: false
migrateMLSCommitLocks: false
```

**How to run migrations independently or in batches**
Expand Down
1 change: 1 addition & 0 deletions hack/helm_vars/common.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ conversationCodesStore: {{ $preferredStore }}
teamFeaturesStore: {{ $preferredStore }}
domainRegistration: {{ $preferredStore }}
userStore: {{ $preferredStore }}
mlsCommitLocksStore: {{ $preferredStore }}

{{- if (eq (env "UPLOAD_XML_S3_BASE_URL") "") }}
uploadXml: {}
Expand Down
1 change: 1 addition & 0 deletions hack/helm_vars/wire-server/values.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ galley:
teamFeatures: {{ .Values.teamFeaturesStore }}
domainRegistration: {{ .Values.domainRegistration }}
user: {{ .Values.userStore }}
mlsCommitLocks: {{ .Values.mlsCommitLocksStore }}
settings:
maxConvAndTeamSize: 16
maxTeamSize: 32
Expand Down
1 change: 1 addition & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ library
Test.Migration.Conversation
Test.Migration.ConversationCodes
Test.Migration.DomainRegistration
Test.Migration.MLSCommitLock
Test.Migration.TeamFeatures
Test.Migration.Util
Test.MLS
Expand Down
74 changes: 74 additions & 0 deletions integration/test/Test/Migration/MLSCommitLock.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Test.Migration.MLSCommitLock where

import Control.Monad.Codensity
import Control.Monad.Reader
import MLS.Util
import SetupHelpers
import Test.Migration.Util (waitForMigration)
import Testlib.Prelude
import Testlib.ResourcePool

-- | Verifies the MLS commit-lock store migration end to end. Every MLS commit
-- acquires and releases the commit lock, so driving commits through the three
-- storage locations exercises the lock in Cassandra, the dual-write mirror, and
-- Postgres-only.
testMLSCommitLockMigration :: (HasCallStack) => App ()
testMLSCommitLockMigration = do
resourcePool <- asks (.resourcePool)
runCodensity (acquireResources 1 resourcePool) $ \[backend] -> do
let domain = backend.berDomain

-- Cassandra: create an MLS group and commit once. This acquires and
-- releases the commit lock against Cassandra.
(alice1, convId) <- runCodensity (startDynamicBackend backend (conf "cassandra" False)) $ \_ -> do
alice <- randomUser domain def
alice1 <- createMLSClient def alice
bob <- randomUser domain def
bob1 <- createMLSClient def bob
void $ uploadNewKeyPackage def bob1
convId <- createNewGroup def alice1
void $ createAddCommit alice1 convId [bob] >>= sendAndConsumeCommitBundle
pure (alice1, convId)

-- Dual-write + backfill: a commit is mirrored to Postgres, and the worker
-- copies any live locks until it reports completion.
runCodensity (startDynamicBackend backend (conf "migration-to-postgresql" True)) $ \_ -> do
charlie <- randomUser domain def
charlie1 <- createMLSClient def charlie
void $ uploadNewKeyPackage def charlie1
void $ createAddCommit alice1 convId [charlie] >>= sendAndConsumeCommitBundle
waitForMigration domain counterName

-- Postgres-only: a commit acquires and releases the lock against Postgres.
runCodensity (startDynamicBackend backend (conf "postgresql" False)) $ \_ -> do
dave <- randomUser domain def
dave1 <- createMLSClient def dave
void $ uploadNewKeyPackage def dave1
void $ createAddCommit alice1 convId [dave] >>= sendAndConsumeCommitBundle

conf :: String -> Bool -> ServiceOverrides
conf db runMigration =
def
{ galleyCfg = setField "postgresMigration.mlsCommitLocks" db,
backgroundWorkerCfg = setField "migrateMLSCommitLocks" runMigration
}

counterName :: String
counterName = "^wire_mls_commit_locks_migration_finished"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE mls_commit_locks (
group_id bytea NOT NULL,
epoch bigint NOT NULL,
expires_at timestamptz NOT NULL,
PRIMARY KEY (group_id, epoch)
);
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,8 @@ acquireCommitLock = "insert into mls_commit_locks (group_id, epoch) values (?, ?

releaseCommitLock :: PrepQuery W (GroupId, Epoch) ()
releaseCommitLock = "delete from mls_commit_locks where group_id = ? and epoch = ?"
selectAllCommitLocks :: PrepQuery R () (GroupId, Epoch)
selectAllCommitLocks = "select group_id, epoch from mls_commit_locks"

-- Bots ---------------------------------------------------------------------

Expand Down
55 changes: 55 additions & 0 deletions libs/wire-subsystems/src/Wire/MLSCommitLockStore/DualWrite.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- This file is part of the Wire Server implementation.
--
-- Copyright (C) 2026 Wire Swiss GmbH <opensource@wire.com>
--
-- This program is free software: you can redistribute it and/or modify it under
-- the terms of the GNU Affero General Public License as published by the Free
-- Software Foundation, either version 3 of the License, or (at your option) any
-- later version.
--
-- This program is distributed in the hope that it will be useful, but WITHOUT
-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
-- details.
--
-- You should have received a copy of the GNU Affero General Public License along
-- with this program. If not, see <https://www.gnu.org/licenses/>.

module Wire.MLSCommitLockStore.DualWrite
( interpretMLSCommitLockStoreToCassandraAndPostgres,
)
where

import Cassandra (ClientState)
import Imports
import Polysemy
import Polysemy.TinyLog (TinyLog)
import Wire.ConversationStore (LockAcquired (..), MLSCommitLockStore (..))
import Wire.ConversationStore qualified as CommitLockStore
import Wire.ConversationStore.Cassandra qualified as Cassandra
import Wire.MLSCommitLockStore.Postgres qualified as Postgres
import Wire.Postgres (PGConstraints)

-- | During migration Cassandra stays the source of truth: every write is
-- mirrored to Postgres, and 'AcquireCommitLock' returns the Cassandra result
-- (the arbiter) so mutual exclusion is governed by a single store until the
-- cutover to 'PostgresqlStorage'.
interpretMLSCommitLockStoreToCassandraAndPostgres ::
( Member TinyLog r,
PGConstraints r
) =>
ClientState ->
InterpreterFor MLSCommitLockStore r
interpretMLSCommitLockStoreToCassandraAndPostgres client = interpret $ \case
AcquireCommitLock gId epoch ttl -> do
-- Cassandra is the arbiter: mirror the acquire to Postgres only when it
-- succeeds, so Postgres never holds a lock Cassandra did not grant.
acquired <- Cassandra.interpretMLSCommitLockStoreToCassandra client $ CommitLockStore.acquireCommitLock gId epoch ttl
when (acquired == Acquired) $
void $
Postgres.interpretMLSCommitLockStoreToPostgres $
CommitLockStore.acquireCommitLock gId epoch ttl
pure acquired
ReleaseCommitLock gId epoch -> do
Cassandra.interpretMLSCommitLockStoreToCassandra client $ CommitLockStore.releaseCommitLock gId epoch
Postgres.interpretMLSCommitLockStoreToPostgres $ CommitLockStore.releaseCommitLock gId epoch
Loading
Loading