@@ -2,6 +2,7 @@ package partitionmigration_test
22
33import (
44 "context"
5+ "database/sql"
56 "fmt"
67 "path/filepath"
78 "strconv"
@@ -55,26 +56,26 @@ func TestPartitionMigrationGatewayProcessorMode(t *testing.T) {
5556 extraStressWorkspaces int // number of extra workspace migrations to include (0 = normal mode)
5657 restartProcessorEvery time.Duration // how often to restart processor nodes while migration is ongoing
5758 jobsDBFanoutEnabled bool // whether source nodes declare per-jobsdb fan-out on migration acknowledgement
59+ forkDestinations bool // fork the destination to the (multi-consumer) proc jobsdb, exercising proc-jobsdb migration
5860 }{
59- {name : "normal" , extraStressWorkspaces : 0 , restartProcessorEvery : 25 * time .Second , jobsDBFanoutEnabled : true },
60- {name : "stress_100_workspaces" , extraStressWorkspaces : 100 , restartProcessorEvery : 30 * time .Second , jobsDBFanoutEnabled : true },
61- {name : "stress_1000_workspaces" , extraStressWorkspaces : 1000 , restartProcessorEvery : 35 * time .Second , jobsDBFanoutEnabled : true },
62- {name : "stress_5000_workspaces" , extraStressWorkspaces : 5000 , restartProcessorEvery : 50 * time .Second , jobsDBFanoutEnabled : true },
63- {name : "legacy_no_jobsdb_fanout" , extraStressWorkspaces : 0 , restartProcessorEvery : 25 * time .Second , jobsDBFanoutEnabled : false },
61+ {name : "normal" , extraStressWorkspaces : 0 , restartProcessorEvery : 25 * time .Second , jobsDBFanoutEnabled : true , forkDestinations : true },
62+ {name : "legacy_no_jobsdb_fanout" , extraStressWorkspaces : 0 , restartProcessorEvery : 25 * time .Second , jobsDBFanoutEnabled : false , forkDestinations : false },
63+ {name : "stress_5000_workspaces" , extraStressWorkspaces : 5000 , restartProcessorEvery : 50 * time .Second , jobsDBFanoutEnabled : true , forkDestinations : false },
6464 } {
6565 t .Run (tc .name , func (t * testing.T ) {
66- testPartitionMigrationGatewayProcessorMode (t , tc .extraStressWorkspaces , tc .restartProcessorEvery , tc .jobsDBFanoutEnabled )
66+ testPartitionMigrationGatewayProcessorMode (t , tc .extraStressWorkspaces , tc .restartProcessorEvery , tc .jobsDBFanoutEnabled , tc . forkDestinations )
6767 })
6868 }
6969}
7070
71- func testPartitionMigrationGatewayProcessorMode (t * testing.T , extraStressWorkspaces int , restartProcessorEvery time.Duration , jobsDBFanoutEnabled bool ) {
71+ func testPartitionMigrationGatewayProcessorMode (t * testing.T , extraStressWorkspaces int , restartProcessorEvery time.Duration , jobsDBFanoutEnabled , forkDestinations bool ) {
7272 const (
73- namespace = "namespace123"
74- workspaceID = "workspace123"
75- sourceID = "source123"
76- destinationID = "destination123"
77- writeKey = "writekey123"
73+ namespace = "namespace123"
74+ workspaceID = "workspace123"
75+ sourceID = "source123"
76+ destinationID = "destination123"
77+ abortDestinationID = "destination-abort-123" // second destination, aborted at the router
78+ writeKey = "writekey123"
7879
7980 numPartitions = 4 // needs to be a power of 2 (e.g., 2, 4, 8, 16, ...)
8081 jobsPerPartitionPerSecond = 50 // number of jobs to send per partition per second from the gateway client
@@ -137,22 +138,31 @@ func testPartitionMigrationGatewayProcessorMode(t *testing.T, extraStressWorkspa
137138 wh := newTestWebhook (t )
138139 t .Cleanup (wh .Close )
139140
140- // start a test backendconfig with 1 source connected to the webhook
141+ // start a test backendconfig with 1 source connected to the webhook. When forking, add a
142+ // second destination on the same source that the router aborts (never delivered), so each
143+ // forked proc job carries two consumers and migration must move all pending consumers.
144+ sourceBuilder := backendconfigtest .NewSourceBuilder ().
145+ WithWorkspaceID (workspaceID ).
146+ WithID (sourceID ).
147+ WithWriteKey (writeKey ).
148+ WithConnection (
149+ backendconfigtest .NewDestinationBuilder ("WEBHOOK" ).
150+ WithID (destinationID ).
151+ WithConfigOption ("webhookMethod" , "POST" ).
152+ WithConfigOption ("webhookUrl" , wh .URL ).
153+ Build ())
154+ if forkDestinations {
155+ sourceBuilder = sourceBuilder .WithConnection (
156+ backendconfigtest .NewDestinationBuilder ("WEBHOOK" ).
157+ WithID (abortDestinationID ).
158+ WithConfigOption ("webhookMethod" , "POST" ).
159+ WithConfigOption ("webhookUrl" , "http://localhost:1234" ). // aborted at the router, never delivered
160+ Build ())
161+ }
141162 bc := backendconfigtest .NewBuilder ().
142163 WithNamespace (namespace , backendconfigtest .NewConfigBuilder ().
143164 WithWorkspaceID (workspaceID ).
144- WithSource (
145- backendconfigtest .NewSourceBuilder ().
146- WithWorkspaceID (workspaceID ).
147- WithID (sourceID ).
148- WithWriteKey (writeKey ).
149- WithConnection (
150- backendconfigtest .NewDestinationBuilder ("WEBHOOK" ).
151- WithID (destinationID ).
152- WithConfigOption ("webhookMethod" , "POST" ).
153- WithConfigOption ("webhookUrl" , wh .URL ).
154- Build ()).
155- Build ()).
165+ WithSource (sourceBuilder .Build ()).
156166 Build ()).
157167 Build ()
158168 bc .URL = strings .Replace (bc .URL , "127.0.0.1" , localIp , 1 ) // replace localhost with local IP for docker containers to access
@@ -271,6 +281,12 @@ func testPartitionMigrationGatewayProcessorMode(t *testing.T, extraStressWorkspa
271281 "Router.Network.IncludeInstanceIdInHeader" : "true" , // for debugging in case of receiving out-of-order events
272282 "Router.jobIterator.maxQueries" : "1" ,
273283 }
284+ if forkDestinations {
285+ // fork both destinations to the proc jobsdb (isolation already enabled above) so each
286+ // proc job carries two consumers; abort the second at the router so it is never delivered
287+ procCommonEnv ["Processor.DestinationIsolation.enabledDestinations.all" ] = "true"
288+ procCommonEnv ["Router.toAbortDestinationIDs" ] = abortDestinationID
289+ }
274290 rsBinaryPath := filepath .Join (t .TempDir (), "rudder-server-binary" )
275291 rudderserver .BuildRudderServerBinary (t , "../../main.go" , rsBinaryPath )
276292 gwNode0Name := "gw-node-0"
@@ -363,6 +379,14 @@ func testPartitionMigrationGatewayProcessorMode(t *testing.T, extraStressWorkspa
363379 // wait for some time to let events flow
364380 time .Sleep (10 * time .Second )
365381
382+ if forkDestinations {
383+ // the fan-out siphon must be active: forked jobs land in the proc jobsdb of some node,
384+ // giving partition migration multi-consumer proc jobs to move
385+ require .Eventually (t , func () bool {
386+ return procJobsCount (t , pg0 .DB )+ procJobsCount (t , pg1 .DB ) > 0
387+ }, 30 * time .Second , 500 * time .Millisecond , "forked jobs should appear in the proc jobsdb" )
388+ }
389+
366390 var srcRouterAcks []string
367391 err = rudoacker .NewSrcrouterAcker (ctx , g , etcdResource .Client , namespace , []string {"srcrouter" }).
368392 WithEventListener (func (key string , value etcdtypes.ReloadSrcRouterCommand ) {
@@ -499,3 +523,13 @@ func restartingProcessorServer(t *testing.T, ctx context.Context, g *errgroup.Gr
499523 }
500524 })
501525}
526+
527+ // procJobsCount returns the number of jobs currently present in the intermediate (proc)
528+ // jobsdb of a node. Used to assert the destination-isolation fan-out siphon is active so
529+ // partition migration has proc jobs to move.
530+ func procJobsCount (t * testing.T , db * sql.DB ) int {
531+ t .Helper ()
532+ var count int
533+ require .NoError (t , db .QueryRow ("SELECT count(DISTINCT job_id) FROM unionjobsdbmetadata('proc', 10)" ).Scan (& count ))
534+ return count
535+ }
0 commit comments