Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
e384c01
brig: Reduce gc_grace_seconds on user tables
akshaymankar Jul 7, 2026
a25ab97
PostgresMarshall/Unmarshall instances for tuples of length 25
akshaymankar Jul 9, 2026
39865c9
UserStore.Migration: Implement migration
akshaymankar Jul 7, 2026
9c68c53
UserStore.Migration: Fix typo, add todo
akshaymankar Jul 13, 2026
c27327a
integration: Add test for user migration to pg
akshaymankar Jul 13, 2026
84bd5e2
background-worker: Run user migration
akshaymankar Jul 28, 2026
8dd9980
integration: Run all phases in user migration test
akshaymankar Jul 28, 2026
917a058
UserStore: Add dummy implementation of the migration interpreter
akshaymankar Jul 28, 2026
7b23d66
integration: Wait max 30s for migrations
akshaymankar Jul 29, 2026
0f76a3d
UserStore: Implement most actions in the migration interpreter
akshaymankar Jul 29, 2026
0951e09
ModService: Silence brig and spar logs
akshaymankar Jul 29, 2026
bed05bc
integration: Use more connections to PG
akshaymankar Jul 29, 2026
962dac9
integration: more consistent user migration config
akshaymankar Jul 29, 2026
c0d1acb
UserStore: Implement glimpse handle for migraiton interpreter
akshaymankar Jul 29, 2026
37261ce
integration: Actually make sure that rich info is tested
akshaymankar Jul 29, 2026
126a117
integration: Reduce number of deletes and new users to speed up the test
akshaymankar Jul 29, 2026
97a6fe6
DomainRegistrationStore.Migration: Delete unused function
akshaymankar Jul 30, 2026
4d83e55
UserStore.Migration: Fix rebase issue
akshaymankar Jul 30, 2026
62a613d
UserStore: Implement listing index users in the migration interpreter
akshaymankar Jul 30, 2026
f5e4750
integration: Test migration of bots
akshaymankar Aug 4, 2026
a2105cd
UserStore: Implement bot operations in the migration interprter
akshaymankar Aug 4, 2026
8c23a87
UserStore.Migration: Cleanup bots correctly
akshaymankar Aug 4, 2026
cf2096b
UserStore.Migration: Handle special deletion of bots
akshaymankar Aug 4, 2026
571a5ec
integration: Minor cleanup
akshaymankar Aug 4, 2026
72976cd
integration: Use random rich info
akshaymankar Aug 20, 2026
f52f7f5
Brig.Data.Activation: Remove debug code
akshaymankar Aug 20, 2026
2710ac1
hlint
akshaymankar Aug 20, 2026
090067b
UserStore.Cassandra: Delete printf debugging
akshaymankar Aug 20, 2026
6bdf071
format
akshaymankar Aug 20, 2026
5bbe0a6
integration: Fix bool blindness issue
akshaymankar Aug 20, 2026
eaf8df6
integration: Delete redundant import, remove -Wwarn
akshaymankar Aug 20, 2026
72e4afe
changelog and docs
akshaymankar Aug 24, 2026
c73f7a4
integration: Fix warnings
akshaymankar Aug 24, 2026
75e9339
integration: Remove impossible TODOs
akshaymankar Aug 24, 2026
c498513
integration: Assert that newly created and previously updated users c…
akshaymankar Aug 24, 2026
52cdf9f
UserStore: Store new users in PG when running in migration mode
akshaymankar Aug 24, 2026
8dcddd3
UserStore: Optimize doesUserExist
akshaymankar Aug 24, 2026
ac81fdc
integration: Add test case for inconsistent handle claims
akshaymankar Aug 24, 2026
f2a4580
UserStore: Deal with handle update during migration
akshaymankar Aug 24, 2026
3707649
integration: Test edge cases of migrating invalid users
akshaymankar Aug 25, 2026
898f1d9
integration: Test migration of user when handle is claimed in Cassand…
akshaymankar Aug 25, 2026
d460a35
pg-migration: Use AtomicState to keep track of errors
akshaymankar Aug 25, 2026
4874885
UserStore.Migration: Gracefully deal with duplicate handle claims
akshaymankar Aug 25, 2026
7616cae
UserStore.Migration: Better logging for users who lose their handles
akshaymankar Aug 25, 2026
849761d
Fix typo
akshaymankar Aug 26, 2026
b993fd8
Add licenses
akshaymankar Aug 26, 2026
48cdc5a
Shorter lines
akshaymankar Aug 26, 2026
c62cffd
Wire.MigrationLock: Sort lock ids to avoid deadlocks
akshaymankar Aug 26, 2026
5b5f52a
integration: Delete invalid users so other tests don't trip over it
akshaymankar Sep 1, 2026
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
10 changes: 5 additions & 5 deletions cassandra-schema.cql
Original file line number Diff line number Diff line change
Expand Up @@ -738,7 +738,7 @@ CREATE TABLE brig_test.rich_info (
AND crc_check_chance = 1.0
AND default_time_to_live = 0
AND extensions = {}
AND gc_grace_seconds = 864000
AND gc_grace_seconds = 86400
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
Expand Down Expand Up @@ -848,7 +848,7 @@ CREATE TABLE brig_test.service_team (
AND crc_check_chance = 1.0
AND default_time_to_live = 0
AND extensions = {}
AND gc_grace_seconds = 864000
AND gc_grace_seconds = 86400
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
Expand All @@ -874,7 +874,7 @@ CREATE TABLE brig_test.service_user (
AND crc_check_chance = 1.0
AND default_time_to_live = 0
AND extensions = {}
AND gc_grace_seconds = 864000
AND gc_grace_seconds = 86400
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
Expand Down Expand Up @@ -1088,7 +1088,7 @@ CREATE TABLE brig_test.user (
AND crc_check_chance = 1.0
AND default_time_to_live = 0
AND extensions = {}
AND gc_grace_seconds = 864000
AND gc_grace_seconds = 86400
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
Expand Down Expand Up @@ -1137,7 +1137,7 @@ CREATE TABLE brig_test.user_handle (
AND crc_check_chance = 1.0
AND default_time_to_live = 0
AND extensions = {}
AND gc_grace_seconds = 864000
AND gc_grace_seconds = 86400
AND max_index_interval = 2048
AND memtable_flush_period_in_ms = 0
AND min_index_interval = 128
Expand Down
1 change: 1 addition & 0 deletions changelog.d/2-features/user-pg-migration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support migrating user data to postgresql from cassandra
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ data:
migrateConversationCodes: {{ .migrateConversationCodes }}
migrateTeamFeatures: {{ .migrateTeamFeatures }}
migrateDomainRegistration: {{ .migrateDomainRegistration }}
migrateUsers: {{ .migrateUsers }}
migrationOptions:
{{ toYaml .migrationOptions | indent 6 }}

Expand Down
4 changes: 4 additions & 0 deletions charts/wire-server/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,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 users
# It's important to set `settings.postgresMigration.users` to `migration-to-postgresql`
# before starting the migration.
migrateUsers: false

backendNotificationPusher:
pushBackoffMinWait: 10000 # in microseconds, so 10ms
Expand Down
2 changes: 1 addition & 1 deletion deploy/dockerephemeral/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ services:
POSTGRES_PASSWORD: "posty-the-gres"
POSTGRES_USER: "wire-server"
POSTGRES_DB: "backendA"
command: postgres -c max_connections=150
command: postgres -c max_connections=1000

cassandra:
container_name: demo_wire_cassandra
Expand Down
20 changes: 18 additions & 2 deletions docs/src/developer/reference/config-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2187,6 +2187,7 @@ The current settings and their background-worker flags are:
- `conversationCodes` -> `migrateConversationCodes`
- `teamFeatures` -> `migrateTeamFeatures`
- `domainRegistration` -> `migrateDomainRegistration`
- `user` -> `migrateUsers`

**Migration pattern per migration setting**

Expand All @@ -2205,13 +2206,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: migration-to-postgresql
user: migration-to-postgresql
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateUsers: false
```

This change should restart the affected pods, and new writes will follow the
Expand All @@ -2226,6 +2229,7 @@ The current settings and their background-worker flags are:
migrateConversationCodes: true
migrateTeamFeatures: true
migrateDomainRegistration: true
migrateUsers: true
```

During migration, Cassandra rows are not deleted. Writes and migration share
Expand All @@ -2241,6 +2245,16 @@ 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`
- `user`: `wire_user_migration_finished`

> ⚠️ For user migrations please watch the logs for `Invalid user found,
> skipping`. This would be accompanied by an error which is either
> `UserHasNoName` or `UserHasNoActivated`. These users are invalid and all
> interactions with them were resulting in errors. If these warnings are
> ignored, these users will stop existing in the system. If these users are
> to be saved, the operator must insert some value as `name` and/or
> `activated` and then re-trigger the migration **after** the background
> worker finishes migrating the valid users.

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 @@ -2253,13 +2267,15 @@ The current settings and their background-worker flags are:
conversation: postgresql
conversationCodes: postgresql
teamFeatures: postgresql
domainRegistration: cassandra
domainRegistration: postgresql
user: postgresql
background-worker:
config:
migrateConversations: false
migrateConversationCodes: false
migrateTeamFeatures: false
migrateDomainRegistration: false
migrateUsers: false
```

**How to run migrations independently or in batches**
Expand Down
2 changes: 2 additions & 0 deletions integration/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
, optparse-applicative
, process
, proto-lens
, QuickCheck
, ram
, random
, raw-strings-qq
Expand Down Expand Up @@ -162,6 +163,7 @@ mkDerivation {
optparse-applicative
process
proto-lens
QuickCheck
ram
random
raw-strings-qq
Expand Down
2 changes: 2 additions & 0 deletions integration/integration.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ library
Test.Migration.ConversationCodes
Test.Migration.DomainRegistration
Test.Migration.TeamFeatures
Test.Migration.User
Test.Migration.Util
Test.MLS
Test.MLS.Clients
Expand Down Expand Up @@ -301,6 +302,7 @@ library
, optparse-applicative
, process
, proto-lens
, QuickCheck ^>=2.15.0.1
, ram
, random
, raw-strings-qq
Expand Down
16 changes: 14 additions & 2 deletions integration/test/API/Brig.hs
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,13 @@ getSelfClients u =

-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_self
deleteUser :: (HasCallStack, MakesValue user) => user -> App Response
deleteUser user = do
deleteUser user = deleteUserWithPassword user (Just defPassword)

deleteUserWithPassword :: (HasCallStack, MakesValue user) => user -> Maybe String -> App Response
deleteUserWithPassword user mPassword = do
req <- baseRequest user Brig Versioned "/self"
submit "DELETE" $
req & addJSONObject ["password" .= defPassword]
req & addJSONObject ["password" .= mPassword]

-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_clients
addClient ::
Expand Down Expand Up @@ -824,6 +827,15 @@ addBot user providerId serviceId convId = do
& zType "access"
& addJSONObject ["provider" .= providerId, "service" .= serviceId]

rmBotSelf :: (HasCallStack, MakesValue domain) => domain -> String -> String -> App Response
rmBotSelf domain bid cid = do
req <- rawBaseRequest domain Brig Versioned $ joinHttpPath ["bot", "self"]
submit "DELETE" $
req
& zType "bot"
& addHeader "Z-Bot" bid
& addHeader "Z-Conversation" cid

setProperty :: (MakesValue user, ToJSON val) => user -> String -> val -> App Response
setProperty user propName val = do
req <- baseRequest user Brig Versioned $ joinHttpPath ["properties", propName]
Expand Down
39 changes: 30 additions & 9 deletions integration/test/API/Common.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import qualified Data.ByteString as BS
import Data.Scientific (scientific)
import qualified Data.Vector as Vector
import System.Random (randomIO, randomRIO)
import Test.QuickCheck
import Testlib.Prelude

-- | please don't use special shell characters like '!' here. it makes writing shell lines
Expand All @@ -33,8 +34,11 @@ defPassword :: String
defPassword = "hunter2."

randomEmail :: App String
randomEmail = do
u <- randomName
randomEmail = liftIO $ generate arbitraryEmail

arbitraryEmail :: Gen String
arbitraryEmail = do
u <- arbitraryName
pure $ u <> "@example.com"

randomDomain :: App String
Expand All @@ -52,23 +56,32 @@ randomExternalId = liftIO $ do
pick = (chars !) <$> randomRIO (Array.bounds chars)

randomName :: App String
randomName = liftIO $ do
n <- randomRIO (8, 15)
randomName = liftIO $ generate arbitraryName

arbitraryName :: Gen String
arbitraryName = do
n <- chooseInt (8, 15)
replicateM n pick
where
chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9']
pick = (chars !) <$> randomRIO (Array.bounds chars)
pick = (chars !) <$> chooseInt (Array.bounds chars)

randomHandle :: App String
randomHandle = randomHandleWithRange 50 256
randomHandle = liftIO $ generate arbitraryHandle

randomHandleWithRange :: Int -> Int -> App String
randomHandleWithRange min' max' = liftIO $ do
n <- randomRIO (min', max')
randomHandleWithRange min' max' = liftIO $ generate (arbitraryHandleWithRange min' max')

arbitraryHandle :: Gen String
arbitraryHandle = arbitraryHandleWithRange 50 60

arbitraryHandleWithRange :: Int -> Int -> Gen String
arbitraryHandleWithRange min' max' = do
n <- chooseInt (min', max')
replicateM n pick
where
chars = mkArray $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-."
pick = (chars !) <$> randomRIO (Array.bounds chars)
pick = (chars !) <$> chooseInt (Array.bounds chars)

randomBytes :: Int -> App ByteString
randomBytes n = liftIO $ BS.pack <$> replicateM n randomIO
Expand All @@ -85,6 +98,14 @@ randomAlphaString n = liftIO $ replicateM n pick
chars = mkArray $ ['A' .. 'Z'] <> ['a' .. 'z'] <> ['0' .. '9']
pick = (chars !) <$> randomRIO (Array.bounds chars)

randomPassword :: App String
randomPassword = liftIO $ generate arbitraryPassword

arbitraryPassword :: Gen String
arbitraryPassword = do
n <- chooseInt (8, 1024)
replicateM n arbitraryPrintableChar

randomJSON :: App Value
randomJSON = do
let maxThings = 5
Expand Down
21 changes: 13 additions & 8 deletions integration/test/SetupHelpers.hs
Original file line number Diff line number Diff line change
Expand Up @@ -605,10 +605,14 @@ getCookieWithSamlLogin ::
App (Maybe String, SAML.SignedAuthnResponse)
getCookieWithSamlLogin mbZHost domain expectSuccess tid nameId mLabel (iid, (meta, privcreds)) = do
let idpConfig = SAML.IdPConfig (SAML.IdPId (fromMaybe (error "invalid idp id") (UUID.fromString iid))) meta ()
spmeta <- getSPMetadataWithZHost domain mbZHost tid
authnreq <- initiateSamlLoginWithZHostAndLabel domain mbZHost mLabel iid
let spMetaData = toSPMetaData spmeta.body
parsedAuthnReq = parseAuthnReqResp authnreq.body
spMetaData <-
getSPMetadataWithZHost domain mbZHost tid `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
pure $ toSPMetaData resp.body
parsedAuthnReq <-
initiateSamlLoginWithZHostAndLabel domain mbZHost mLabel iid `bindResponse` \resp -> do
resp.status `shouldMatchInt` 200
pure $ parseAuthnReqResp resp.body
authnReqResp <- makeAuthnResponse nameId privcreds idpConfig spMetaData parsedAuthnReq
mCookie <- finalizeSamlLoginWithZHost domain mbZHost tid authnReqResp `bindResponse` validateLoginResp
pure (mCookie, authnReqResp)
Expand Down Expand Up @@ -653,13 +657,14 @@ makeAuthnResponse nameId privcreds idpConfig spMetaData parsedAuthnReq =

-- | extract an `AuthnRequest` from the html form in the http response from /sso/initiate-login
parseAuthnReqResp ::
(HasCallStack) =>
ByteString ->
SAML.AuthnRequest
parseAuthnReqResp bs = reqBody
where
xml :: XML.Document
xml =
fromRight (error "malformed html in response body") $
fromRight (error $ "malformed html in response body: \n" <> show bs) $
XML.parseText XML.def (cs bs)

reqBody :: SAML.AuthnRequest
Expand Down Expand Up @@ -855,12 +860,12 @@ createNewIndex = do
ExitFailure _ -> assertFailure $ prefix <> "failed to create index"
ExitSuccess -> pure indexName

reindexUsers :: (HasCallStack) => BackendResource -> Int -> App ()
reindexUsers ber pageSize = do
reindexUsers :: (HasCallStack) => BackendResource -> ServiceOverrides -> Int -> App ()
reindexUsers ber serviceOverrides pageSize = do
testName <- asks (fromMaybe "NoTest" . (.currentTestName))
let indexName = ber.berElasticsearchIndex
let prefix = "[reindex-users:" <> indexName <> ":" <> testName <> "] "
getBrigConfig <- readAndUpdateConfig (defaultOverrides ber) ber Brig
getBrigConfig <- readAndUpdateConfig (defaultOverrides ber <> serviceOverrides) ber Brig
brigConfig <- liftIO $ getBrigConfig
esServer <- brigConfig %. "elasticsearch.url" & asString
esCredentials <- brigConfig %. "elasticsearch.credentials" & asString
Expand Down
1 change: 1 addition & 0 deletions integration/test/Test/Migration/Conversation.hs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ testMigrationToPostgresMLS = do
runPhase 5
where
n = 1

createTestConvs :: (HasCallStack) => ClientIdentity -> String -> ClientIdentity -> ClientIdentity -> [ClientIdentity] -> App TestConvList
createTestConvs creatorC tid melC markC othersC = do
unmodifiedConvs <- replicateM n $ do
Expand Down
Loading
Loading