Skip to content

Commit 462ab74

Browse files
authored
CORE-18913 extract the group allocator logic in the mediator to its own service (#5257)
This allows for better unit testing of the allocator logic and cleaner code in the mediator.
1 parent 63b69f7 commit 462ab74

File tree

13 files changed

+221
-40
lines changed

13 files changed

+221
-40
lines changed

components/flow/flow-mapper-service/src/integrationTest/kotlin/net/corda/session/mapper/service/integration/FlowMapperServiceIntegrationTest.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,10 @@ class FlowMapperServiceIntegrationTest {
520520
producer {
521521
close.timeout = 6000
522522
}
523+
mediator {
524+
poolSize = 1
525+
minPoolRecordCount = 20
526+
}
523527
pollTimeout = 100
524528
}
525529
"""

components/flow/flow-mapper-service/src/integrationTest/kotlin/net/corda/session/mapper/service/integration/TestFlowEventMediatorFactoryImpl.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class TestFlowEventMediatorFactoryImpl @Activate constructor(
7979
.threads(1)
8080
.threadName("flow-event-mediator")
8181
.stateManager(stateManagerFactory.create(stateManagerConfig))
82+
.minGroupSize(20)
8283
.build()
8384

8485
private fun createMessageRouterFactory() = MessageRouterFactory { clientFinder ->

components/flow/flow-mapper-service/src/main/kotlin/net/corda/session/mapper/messaging/mediator/FlowMapperEventMediatorFactoryImpl.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import net.corda.schema.Schemas.Flow.FLOW_MAPPER_START
2323
import net.corda.schema.Schemas.Flow.FLOW_SESSION
2424
import net.corda.schema.Schemas.Flow.FLOW_START
2525
import net.corda.schema.Schemas.P2P.P2P_OUT_TOPIC
26-
import net.corda.schema.configuration.FlowConfig
26+
import net.corda.schema.configuration.MessagingConfig.Subscription.PROCESSING_MIN_POOL_RECORD_COUNT
27+
import net.corda.schema.configuration.MessagingConfig.Subscription.PROCESSING_THREAD_POOL_SIZE
2728
import net.corda.session.mapper.service.executor.FlowMapperMessageProcessor
2829
import org.osgi.service.component.annotations.Activate
2930
import org.osgi.service.component.annotations.Component
@@ -51,15 +52,13 @@ class FlowMapperEventMediatorFactoryImpl @Activate constructor(
5152
stateManager: StateManager,
5253
) = eventMediatorFactory.create(
5354
createEventMediatorConfig(
54-
flowConfig,
5555
messagingConfig,
5656
FlowMapperMessageProcessor(flowMapperEventExecutorFactory, flowConfig),
5757
stateManager,
5858
)
5959
)
6060

6161
private fun createEventMediatorConfig(
62-
flowConfig: SmartConfig,
6362
messagingConfig: SmartConfig,
6463
messageProcessor: StateAndEventProcessor<String, FlowMapperState, FlowMapperEvent>,
6564
stateManager: StateManager,
@@ -84,9 +83,10 @@ class FlowMapperEventMediatorFactoryImpl @Activate constructor(
8483
)
8584
.messageProcessor(messageProcessor)
8685
.messageRouterFactory(createMessageRouterFactory())
87-
.threads(flowConfig.getInt(FlowConfig.PROCESSING_THREAD_POOL_SIZE))
86+
.threads(messagingConfig.getInt(PROCESSING_THREAD_POOL_SIZE))
8887
.threadName("flow-mapper-event-mediator")
8988
.stateManager(stateManager)
89+
.minGroupSize(messagingConfig.getInt(PROCESSING_MIN_POOL_RECORD_COUNT))
9090
.build()
9191

9292
private fun createMessageRouterFactory() = MessageRouterFactory { clientFinder ->

components/flow/flow-mapper-service/src/test/kotlin/net/corda/session/mapper/service/messaging/mediator/FlowMapperEventMediatorFactoryImplTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import net.corda.messaging.api.mediator.config.EventMediatorConfig
88
import net.corda.messaging.api.mediator.factory.MediatorConsumerFactoryFactory
99
import net.corda.messaging.api.mediator.factory.MessagingClientFactoryFactory
1010
import net.corda.messaging.api.mediator.factory.MultiSourceEventMediatorFactory
11-
import net.corda.schema.configuration.FlowConfig
11+
import net.corda.schema.configuration.MessagingConfig
1212
import net.corda.session.mapper.messaging.mediator.FlowMapperEventMediatorFactory
1313
import net.corda.session.mapper.messaging.mediator.FlowMapperEventMediatorFactoryImpl
1414
import org.junit.jupiter.api.Assertions.assertNotNull
@@ -24,13 +24,13 @@ class FlowMapperEventMediatorFactoryImplTest {
2424
private val mediatorConsumerFactoryFactory = mock<MediatorConsumerFactoryFactory>()
2525
private val messagingClientFactoryFactory = mock<MessagingClientFactoryFactory>()
2626
private val multiSourceEventMediatorFactory = mock<MultiSourceEventMediatorFactory>()
27-
private val flowConfig = mock<SmartConfig>()
27+
private val config = mock<SmartConfig>()
2828

2929
@BeforeEach
3030
fun beforeEach() {
3131
`when`(multiSourceEventMediatorFactory.create(any<EventMediatorConfig<String, FlowMapperState, FlowMapperEvent>>()))
3232
.thenReturn(mock())
33-
`when`(flowConfig.getInt(FlowConfig.PROCESSING_THREAD_POOL_SIZE)).thenReturn(10)
33+
`when`(config.getInt(MessagingConfig.Subscription.PROCESSING_THREAD_POOL_SIZE)).thenReturn(10)
3434

3535
flowMapperEventMediatorFactory = FlowMapperEventMediatorFactoryImpl(
3636
flowMapperEventExecutorFactory,
@@ -42,7 +42,7 @@ class FlowMapperEventMediatorFactoryImplTest {
4242

4343
@Test
4444
fun `successfully creates event mediator`() {
45-
val mediator = flowMapperEventMediatorFactory.create(flowConfig, mock(), mock())
45+
val mediator = flowMapperEventMediatorFactory.create(mock(), config, mock())
4646

4747
assertNotNull(mediator)
4848
}

components/flow/flow-service/src/main/kotlin/net/corda/flow/messaging/mediator/FlowEventMediatorFactoryImpl.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import net.corda.data.uniqueness.UniquenessCheckRequestAvro
1313
import net.corda.flow.pipeline.factory.FlowEventProcessorFactory
1414
import net.corda.ledger.utxo.verification.TransactionVerificationRequest
1515
import net.corda.libs.configuration.SmartConfig
16-
import net.corda.libs.configuration.helper.getConfig
1716
import net.corda.libs.platform.PlatformInfoProvider
1817
import net.corda.libs.statemanager.api.StateManager
1918
import net.corda.messaging.api.constants.WorkerRPCPaths.CRYPTO_PATH
@@ -43,8 +42,8 @@ import net.corda.schema.configuration.BootConfig.PERSISTENCE_WORKER_REST_ENDPOIN
4342
import net.corda.schema.configuration.BootConfig.TOKEN_SELECTION_WORKER_REST_ENDPOINT
4443
import net.corda.schema.configuration.BootConfig.UNIQUENESS_WORKER_REST_ENDPOINT
4544
import net.corda.schema.configuration.BootConfig.VERIFICATION_WORKER_REST_ENDPOINT
46-
import net.corda.schema.configuration.ConfigKeys
47-
import net.corda.schema.configuration.FlowConfig
45+
import net.corda.schema.configuration.MessagingConfig.Subscription.PROCESSING_MIN_POOL_RECORD_COUNT
46+
import net.corda.schema.configuration.MessagingConfig.Subscription.PROCESSING_THREAD_POOL_SIZE
4847
import org.osgi.service.component.annotations.Activate
4948
import org.osgi.service.component.annotations.Component
5049
import org.osgi.service.component.annotations.Reference
@@ -79,15 +78,13 @@ class FlowEventMediatorFactoryImpl @Activate constructor(
7978
stateManager: StateManager,
8079
) = eventMediatorFactory.create(
8180
createEventMediatorConfig(
82-
configs,
8381
messagingConfig,
8482
flowEventProcessorFactory.create(configs),
8583
stateManager,
8684
)
8785
)
8886

8987
private fun createEventMediatorConfig(
90-
configs: Map<String, SmartConfig>,
9188
messagingConfig: SmartConfig,
9289
messageProcessor: StateAndEventProcessor<String, Checkpoint, FlowEvent>,
9390
stateManager: StateManager,
@@ -115,9 +112,10 @@ class FlowEventMediatorFactoryImpl @Activate constructor(
115112
)
116113
.messageProcessor(messageProcessor)
117114
.messageRouterFactory(createMessageRouterFactory(messagingConfig))
118-
.threads(configs.getConfig(ConfigKeys.FLOW_CONFIG).getInt(FlowConfig.PROCESSING_THREAD_POOL_SIZE))
115+
.threads(messagingConfig.getInt(PROCESSING_THREAD_POOL_SIZE))
119116
.threadName("flow-event-mediator")
120117
.stateManager(stateManager)
118+
.minGroupSize(messagingConfig.getInt(PROCESSING_MIN_POOL_RECORD_COUNT))
121119
.build()
122120

123121
private fun createMessageRouterFactory(messagingConfig: SmartConfig) = MessageRouterFactory { clientFinder ->

components/flow/flow-service/src/test/kotlin/net/corda/flow/messaging/FlowEventMediatorFactoryImplTest.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import net.corda.schema.Schemas.Flow.FLOW_EVENT_TOPIC
3232
import net.corda.schema.Schemas.Flow.FLOW_MAPPER_SESSION_OUT
3333
import net.corda.schema.Schemas.Flow.FLOW_STATUS_TOPIC
3434
import net.corda.schema.configuration.ConfigKeys
35-
import net.corda.schema.configuration.FlowConfig
35+
import net.corda.schema.configuration.MessagingConfig
3636
import org.assertj.core.api.Assertions.assertThat
3737
import org.junit.jupiter.api.Assertions.assertNotNull
3838
import org.junit.jupiter.api.BeforeEach
@@ -51,7 +51,7 @@ class FlowEventMediatorFactoryImplTest {
5151
private val multiSourceEventMediatorFactory = mock<MultiSourceEventMediatorFactory>()
5252
private val cordaAvroSerializationFactory = mock<CordaAvroSerializationFactory>()
5353
private val platformInfoProvider = mock<PlatformInfoProvider>()
54-
private val flowConfig = mock<SmartConfig>()
54+
private val config = mock<SmartConfig>()
5555

5656
val captor = argumentCaptor<EventMediatorConfig<String, Checkpoint, FlowEvent>>()
5757

@@ -63,7 +63,7 @@ class FlowEventMediatorFactoryImplTest {
6363
`when`(multiSourceEventMediatorFactory.create(captor.capture()))
6464
.thenReturn(mock())
6565

66-
`when`(flowConfig.getInt(FlowConfig.PROCESSING_THREAD_POOL_SIZE)).thenReturn(10)
66+
`when`(config.getInt(MessagingConfig.Subscription.PROCESSING_THREAD_POOL_SIZE)).thenReturn(10)
6767

6868
flowEventMediatorFactory = FlowEventMediatorFactoryImpl(
6969
flowEventProcessorFactory,
@@ -83,7 +83,7 @@ class FlowEventMediatorFactoryImplTest {
8383
@Test
8484
fun `successfully creates event mediator with expected routes`() {
8585
val mediator = flowEventMediatorFactory.create(
86-
mapOf(ConfigKeys.FLOW_CONFIG to flowConfig),
86+
mapOf(ConfigKeys.MESSAGING_CONFIG to config),
8787
mock(),
8888
mock(),
8989
)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ commonsLangVersion = 3.12.0
4444
commonsTextVersion = 1.10.0
4545
# Corda API libs revision (change in 4th digit indicates a breaking change)
4646
# Change to 5.2.0.xx-SNAPSHOT to pick up maven local published copy
47-
cordaApiVersion=5.2.0.16-beta+
47+
cordaApiVersion=5.2.0.17-beta+
4848

4949
disruptorVersion=3.4.4
5050
felixConfigAdminVersion=1.9.26
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package net.corda.messaging.mediator
2+
3+
import net.corda.messaging.api.mediator.config.EventMediatorConfig
4+
import net.corda.messaging.api.records.Record
5+
import kotlin.math.ceil
6+
import kotlin.math.min
7+
8+
/**
9+
* Helper class to use in the mediator to divide polled records into groups for processing.
10+
*/
11+
class GroupAllocator {
12+
13+
/**
14+
* Allocate events into groups based on their keys, a configured minimum group size and thread count.
15+
* This allows for more efficient multi-threaded processing.
16+
* The threshold record count to establish a new group is [config.minGroupSize].
17+
* If the number of groups exceeds the number of threads then the group count is set to the number of [config.threads]
18+
* Records of the same key are always placed into the same group regardless of group size and count.
19+
* @param events Events to allocate to groups
20+
* @param config Mediator config
21+
* @return Records allocated to groups.
22+
*/
23+
fun <K : Any, S : Any, E : Any> allocateGroups(
24+
events: List<Record<K, E>>,
25+
config: EventMediatorConfig<K, S, E>
26+
): List<Map<K, List<Record<K, E>>>> {
27+
val groups = setUpGroups(config, events)
28+
val buckets = events
29+
.groupBy { it.key }.toList()
30+
.sortedByDescending { it.second.size }
31+
32+
buckets.forEach { (key, records) ->
33+
val leastFilledGroup = groups.minByOrNull { it.values.flatten().size }
34+
leastFilledGroup?.put(key, records)
35+
}
36+
37+
return groups.filter { it.values.isNotEmpty() }
38+
}
39+
40+
private fun <E : Any, S: Any, K : Any> setUpGroups(
41+
config: EventMediatorConfig<K, S, E>,
42+
events: List<Record<K, E>>
43+
): MutableList<MutableMap<K, List<Record<K, E>>>> {
44+
val numGroups = min(
45+
ceil(events.size.toDouble() / config.minGroupSize).toInt(),
46+
config.threads
47+
)
48+
49+
return MutableList(numGroups) { mutableMapOf() }
50+
}
51+
}

libs/messaging/messaging-impl/src/main/kotlin/net/corda/messaging/mediator/MultiSourceEventMediatorImpl.kt

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class MultiSourceEventMediatorImpl<K : Any, S : Any, E : Any>(
5555
private val taskManagerHelper = TaskManagerHelper(
5656
taskManager, stateManagerHelper, metrics
5757
)
58+
private val groupAllocator = GroupAllocator()
5859
private val uniqueId = UUID.randomUUID().toString()
5960
private val lifecycleCoordinatorName = LifecycleCoordinatorName(
6061
"MultiSourceEventMediator--${config.name}", uniqueId
@@ -164,7 +165,7 @@ class MultiSourceEventMediatorImpl<K : Any, S : Any, E : Any>(
164165
val messages = consumer.poll(pollTimeout)
165166
val startTimestamp = System.nanoTime()
166167
if (messages.isNotEmpty()) {
167-
var groups = allocateGroups(messages.map { it.toRecord() })
168+
var groups = groupAllocator.allocateGroups(messages.map { it.toRecord() }, config)
168169
var states = stateManager.get(messages.map { it.key.toString() }.distinct())
169170

170171
while (groups.isNotEmpty()) {
@@ -230,7 +231,7 @@ class MultiSourceEventMediatorImpl<K : Any, S : Any, E : Any>(
230231
states = failedToCreate + failedToDelete + failedToUpdateOptimisticLockFailure
231232

232233
groups = if (states.isNotEmpty()) {
233-
allocateGroups(flowEvents.filterKeys { states.containsKey(it) }.values.flatten())
234+
groupAllocator.allocateGroups(flowEvents.filterKeys { states.containsKey(it) }.values.flatten(), config)
234235
} else {
235236
listOf()
236237
}
@@ -319,23 +320,4 @@ class MultiSourceEventMediatorImpl<K : Any, S : Any, E : Any>(
319320
}
320321
}
321322
}
322-
323-
private fun allocateGroups(events: List<Record<K, E>>): List<Map<K, List<Record<K, E>>>> {
324-
val groups = mutableListOf<MutableMap<K, List<Record<K, E>>>>()
325-
val groupCountBasedOnEvents = (events.size / 20).coerceAtLeast(1)
326-
val groupsCount = if (groupCountBasedOnEvents < config.threads) groupCountBasedOnEvents else config.threads
327-
for (i in 0 until groupsCount) {
328-
groups.add(mutableMapOf())
329-
}
330-
val buckets = events.groupBy { it.key }
331-
val bucketSizes = buckets.keys.sortedByDescending { buckets[it]?.size }
332-
for (i in buckets.size - 1 downTo 0 step 1) {
333-
val group = groups.minBy { it.values.flatten().size }
334-
val key = bucketSizes[i]
335-
val records = buckets[key]!!
336-
group[key] = records
337-
}
338-
339-
return groups
340-
}
341323
}

0 commit comments

Comments
 (0)