How to aggregate the elements in the window sessions flink? - kotlin

I'm using the flink Session windows when it does not receive elements for a certain period of time,i.e; when a gap of inactivity occurred it should emit an event.
I configured the gap as 10 seconds in the flink job. And I sent the event1 and sends event2 after 5 seconds. These two events should belong to first window. The output should be an aggregate of these two events. But I am getting only the first event.
below is the code I tried:
fun setupJob(env: StreamExecutionEnvironment) {
val testStream = env.sampleStream()
.keyBy { it.f0 }
.window(EventTimeSessionWindows.withGap(Time.seconds(10)))
.process(MyProcessWindowFunction())
testStream.map { it.toKafkaMessage() }
.kafkaSink<SampleOutput>() }
}
then MyProcessWindowFunction looks like
class MyProcessWindowFunction : ProcessWindowFunction<Tuple4<String, inputA?, inputB?, inputC?>, Tuple2<String, SampleOutput?>,
String, TimeWindow>() {
private lateinit var sampleOutputState: ValueState<SampleOutputState>
override fun open(parameters: Configuration) {
val SampleOutputStateDescriptor = ValueStateDescriptor("sample-output-state", SampleOutputState::class.java)
SampleOutputState = runtimeContext.getState(SampleOutputStateDescriptor)
}
override fun process(key: String, context: Context, elements: MutableIterable<Tuple4<String, inputA?, inputB?, inputC?>, out: Collector<Tuple2<String, SampleOutput?>>) {
val current = sampleOutputState.value()
val value = elements.iterator().next()
val latestState = when {
value.f2 != null -> processCondition(value.f2!!, current)
else -> return
}
sampleOutputState.update(latestState)
out.collect(Tuple2(key, latestState))
}
private fun processInputB(inputB: InputB, currentState: SampleOutputState?): SampleOutputState {
return currentState?.copy(
timestamp = System.currentTimeMillis(),
eventTime = condition.eventTime,
) ?:
createInputBState(inputB)
}
private fun createInputBState(inputB: InputB): SampleOutputState = SampleOutputState(
id = UUID.randomUUID().toString(),
timestamp = System.currentTimeMillis(),
eventTime = condition.eventTime,
)
}
I'm getting the only event1 but I wanted to get the aggregate of both the events (that I sent event1 and event2).
How do we get the aggregate of the events that are available with in a session ?

All of the events assigned to the window will be in the iterable sent to the process method of your ProcessWindowFunction. You are currently only looking at the first element with
val value = elements.iterator().next()
You need to iterate through elements in order to produce an aggregated result.

Related

Can you change the color of a textview in a recyclerview adapter after a certain condition is met in Main Activity?

I have a basic function that displays the elapsed time every time the button is pressed. I cannot get the logic in MainActivity to transfer to the recyclerview adapter. I simply want the text output color to change to red after the time passes 5 seconds. I have tried to research how to do this for the past week and I cannot find the exact answer. I'm hoping someone can help.
I have tried it with and without the boolean in the data class. I wasn't sure if that was required.
Here is my code:
Main Activity:`
class MainActivity : AppCompatActivity() {
var startTime = SystemClock.elapsedRealtime()
var displaySeconds = 0
private lateinit var binding: ActivityMainBinding
private val secondsList = generateSecondsList()
private val secondsAdapter = Adapter(secondsList)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
recyclerView.adapter = secondsAdapter
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.setHasFixedSize(false)
binding.button.setOnClickListener {
getDuration()
addSecondsToRecyclerView()
}
}
fun getDuration(): Int {
val endTime = SystemClock.elapsedRealtime()
val elapsedMilliSeconds: Long = endTime - startTime
val elapsedSeconds = elapsedMilliSeconds / 1000.0
displaySeconds = elapsedSeconds.toInt()
return displaySeconds
}
private fun generateSecondsList(): ArrayList<Seconds> {
return ArrayList()
}
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
Adapter:
var adapterSeconds = MainActivity().getDuration()
class Adapter(
private val rvDisplay: MutableList<Seconds>
) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
class AdapterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val textView1: TextView = itemView.tv_seconds
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AdapterViewHolder {
val myItemView = LayoutInflater.from(parent.context).inflate(
R.layout.rv_item,
parent, false
)
return AdapterViewHolder(myItemView)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
val currentDisplay = rvDisplay[position]
currentDisplay.isRed = adapterSeconds > 5
holder.itemView.apply {
val redColor = ContextCompat.getColor(context, R.color.red).toString()
val blackColor = ContextCompat.getColor(context, R.color.black).toString()
if (currentDisplay.isRed) {
holder.textView1.setTextColor(redColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
} else {
holder.textView1.setTextColor(blackColor.toInt())
holder.textView1.text = currentDisplay.rvSeconds.toString()
}
}
}
override fun getItemCount() = rvDisplay.size
}
Data Class:
data class Seconds(
var rvSeconds: Int,
var isRed: Boolean
)
when you call secondsList.add(addSeconds) then the data that is already inside secondsList should be updated too.
you could do something like
private var secondsList = generateSecondsList() // make this var
fun addSecondsToRecyclerView() {
val addSeconds =
Seconds(getDuration(), true)
secondsList.add(addSeconds)
if ( /* TODO check if time has passed */) {
secondsList = secondsList.map { it.isRed = true }
secondsAdapter.rvDisplay = secondsList // TODO also make rvDisplay a var
secondsAdapter.notifyDatasetChanged() // also need to tell rv to redraw the all views
} else {
secondsAdapter.notifyItemInserted(secondsList.size - 1)
}
}
that might work, but to be honest it looks bad... There is already a lot of logic inside Activity. Read about MVVM architecture and LiveData, there should be another class called ViewModel that would keep track of time and the data. Activity should be as simple as possible, because it has lifecycle, so if you rotate the screen, all your state will be lost.
Your code isn't really working because of this:
var adapterSeconds = MainActivity().getDuration()
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
...
currentDisplay.isRed = adapterSeconds > 5
...
}
You're only setting adapterSeconds right there, so it never updates as time passes. I assume you want to know the moment 5 seconds has elapsed, and then update the RecyclerView at that moment - in that case you'll need some kind of timer task that will fire after 5 seconds, and can tell the adapter to display things as red. Let's deal with that first:
class Adapter( private val rvDisplay: MutableList ) : RecyclerView.Adapter<Adapter.AdapterViewHolder>() {
private var displayRed = false
set(value) {
field = value
// Refresh the display - the ItemChanged methods mean something about the items
// has changed, rather than a structural change in the list
// But you can use notifyDataSetChanged if you want (better to be specific though)
notifyItemRangeChanged(0, itemCount)
}
override fun onBindViewHolder(holder: Adapter.AdapterViewHolder, position: Int) {
if (displayRed) {
// show things as red - you shouldn't need to store that state in the items
// themselves, it's not about them - it's an overall display state, right?
} else {
// display as not red
}
}
So with that setter function, every time you update displayRed it'll refresh the display, which calls onBindViewHolder, which checks displayRed to see how to style things. It's better to put all this internal refreshing stuff inside the adapter - just pass it data and events, let it worry about what needs to happen internally and to the RecyclerView it's managing, y'know?
Now we have a thing we can set to control how the list looks, you just need a timer to change it. Lots of ways to do this - a CountdownTimer, a coroutine, but let's keep things simple for this example and just post a task to the thread's Looper. We can do that through any View instead of creating a Handler:
// in MainActivity
recyclerView.postDelayed({ secondsAdapter.displayRed = true }, 5000)
That's it! Using any view, post a delayed function that tells the adapter to display as red.
It might be more helpful to store that runnable as an object:
private val showRedTask = Runnable { secondsAdapter.displayRed = true }
...
recyclerView.postDelayed(showRedTask, 5000)
because then you can easily cancel it
recyclerView.removeCallbacks(showRedTask)
Hopefully that's enough for you to put some logic together to get what you want. Set displayRed = false to reset the styling, use removeCallbacks to cancel any running task, and postDelayed to start a new countdown. Not the only way to do it, but it's pretty neat!
I finally figured it out using a companion object in Main Activity with a boolean set to false. If the time exceeded 5 seconds, then it set to true.
The adapter was able to recognize the companion object and change the color of seconds to red if they exceeded 5.

Rerun StateFlow When Filter Needs to Change?

I have a StateFlow containing a simple list of strings. I want to be able to filter that list of Strings. Whenever the filter gets updated, I want to push out a new update to the StateFlow.
class ResultsViewModel(
private val api: API
) : ViewModel() {
var filter: String = ""
val results: StateFlow<List<String>> = api.resultFlow()
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
}
It's easy enough to stick a map onto api.resultFlow():
val results: StateFlow<List<String>> = api.resultFlow()
.map { result ->
val filtered = mutableListOf<String>()
for (r in result) {
if (r.contains(filter)) {
filtered.add(r)
}
}
filtered
}
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
But how do I get the flow to actually emit an update when filter changes? Right now, this only works with whatever the initial value of filter is set to.
You could have the filter update another StateFlow that is combined with the other one. By the way, there's filter function that is easier to use than manually creating another list and iterating to get your results.
class ResultsViewModel(
private val api: API
) : ViewModel() {
private val filterFlow = MutableStateFlow("")
var filter: String
get() = filterFlow.value
set(value) {
filterFlow.value = value
}
val results: StateFlow<List<String>> =
api.resultFlow()
.combine(filterFlow) { list, filter ->
list.filter { it.contains(filter) }
}
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
}

How to find last node that satisfies where predicate in singly linked list?

write a method "lastWhere" that accepts a function called "where" of type (T) -> Boolean. The method returns the last element of type T to which the "where" function applies. If no matching element is found, null is returned.
call the method "lastwhere" on the linked list below. Find the last game that is more than 10 euros.
So far I've got this Code going for me.
I assume the only important piece of Code I need to edit is the "fun lastWhere" for task number 1)
the second task wants me to implement a way on the main function to find the last Game that is cheaper than 10 Euros.
class LinkedList<T> {
data class Node<T>(val data: T, var next: Node<T>?)
private var first: Node<T>? = null
override fun toString(): String = first?.toString() ?: "-"
fun isEmpty() = first == null
fun addLast(data: T) {
if (first == null) {
first = Node(data, first)
return
}
var runPointer = first
while (runPointer?.next != null) {
runPointer = runPointer.next
}
runPointer?.next = Node(data, null)
}
fun lastWhere (where: (T) -> Boolean): T? { // "where" function needs to be implemented
if (isEmpty()) return null
else {
var runPointer = first
while (runPointer?.next != null ) {
runPointer = runPointer.next
}
return runPointer?.data
}
}
}
data class Game(val title: String, val price: Double)
fun main() {
val list = LinkedList<Game>()
list.addLast(Game("Minecraft", 9.99))
list.addLast(Game("Overwatch", 29.99))
list.addLast(Game("Mario Kart", 59.99))
list.addLast(Game("World of Warcraft", 19.99))
var test = list.lastWhere ({it.price >= 10.00}) // This is probably wrong too, since I haven't got task 1) working
println (test)
}
Would appreciate any help!
Since you only store a reference to first node, you don't have any choice but to start at first and iterate. you will also have to keep a reference to last item that satisfied the where predicate, and keep updating this reference with every iteration.
fun lastWhere (where: (T) -> Boolean): T? {
var runPointer = first
var item: T? = null // init item to null, if nothing is found we return null
while (runPointer != null ) {
// For every node, execute the where function and if it returns true
// then update the return value
if(where(runPointer.data)) { item = runPointer.data }
runPointer = runPointer.next
}
return item
}

Beam JdbcIO.readAll doesn't seem to return results

I am trying to build pipelines for events with Apache Beam. What I wanted to do is reading streaming data from GCP PubSub and read related metadata from MySQL using the ids in the events, then combine those two streams and write to my clickhouse database.
But JdbcIO.readall() doesn't seem to return its stream. As you can see on the ClickhousePipeline, after applying CoGroupByKey.create(), I am trying to combine two PCollection, but userMetaData comes in empty, and ParDo that chained right after UserMetadataEnricher() hasn't executed too.
In withRowMapper on UserMetadataEnricher, I added println() to chech if it's running, and It worked properly and print the results from my database, however, It doesn't return data to the next pipeline.
I guess the problem is related to Windowing, I checked it's working when I tested it without windowing. But, PubSubIO is Unbounded PCollection, so I have to apply window to use JDBCIO.readall() right? I have no clue to solve this problem. I hope to get an answer soon!
MainPipeline
object MainPipeline {
#JvmStatic
fun run(options: MainPipelineOptions) {
val p = Pipeline.create(options)
val events = p
.apply(
"Read DetailViewEvent PubSub",
PubsubIO.readStrings().fromSubscription(options.inputSubscription)
)
.apply(
"Extract messages",
ParseJsons.of(FoodDetailViewEvent::class.java)
.exceptionsInto(
TypeDescriptors.kvs(TypeDescriptors.strings(), TypeDescriptors.strings())
)
.exceptionsVia { KV.of(it.element(), it.exception().javaClass.canonicalName) }
)
val validEvents =
events.output().setCoder(SerializableCoder.of(FoodDetailViewEvent::class.java))
val invalidEvents = events.failures()
invalidEvents.apply(FailurePipeline(options))
validEvents.apply(ClickhousePipeline(options))
p.run().waitUntilFinish()
}
#JvmStatic
fun main(args: Array<String>) {
val options = PipelineOptionsFactory
.fromArgs(*args)
.withValidation()
.`as`(MainPipelineOptions::class.java)
run(options)
}
}
ClickhousePipeline
class ClickhousePipeline(private val options: MainPipelineOptions) :
PTransform<PCollection<DetailViewEvent>, PDone>() {
override fun expand(events: PCollection<DetailViewEvent>): PDone {
val windowedEvents = events
.apply(
"Window", Window
.into<DetailViewEvent>(GlobalWindows())
.triggering(
Repeatedly
.forever(
AfterProcessingTime
.pastFirstElementInPane()
.plusDelayOf(Duration.standardSeconds(5))
)
)
.withAllowedLateness(Duration.ZERO).discardingFiredPanes()
)
val userIdDetailViewEvents = windowedEvents
.apply(
MapElements.via(object :
SimpleFunction<DetailViewEvent, KV<String, DetailViewEvent>>() {
override fun apply(input: DetailViewEvent): KV<String, DetailViewEvent> {
return KV.of(input.userInfo.userId, input)
}
})
)
val userMetaData = userIdDetailViewEvents
.apply(
MapElements.via(object :
SimpleFunction<KV<String, DetailViewEvent>, String>() {
override fun apply(input: KV<String, DetailViewEvent>): String {
return input.key!!
}
})
)
.apply(
UserMetadataEnricher(options)
)
.apply(
ParDo.of(
object : DoFn<UserMetadata, KV<String, UserMetadata>>() {
#ProcessElement
fun processElement(
#Element data: UserMetadata,
out: OutputReceiver<KV<String, UserMetadata>>
) {
println("User:: ${data}") // Not printed!!
out.output(KV.of(data.userId, data))
}
})
)
val sourceTag = object : TupleTag<DetailViewEvent>() {}
val userMetadataTag = object : TupleTag<UserMetadata>() {}
val joinedPipeline: PCollection<KV<String, CoGbkResult>> =
KeyedPCollectionTuple.of(sourceTag, userIdDetailViewEvents)
.and(userMetadataTag, userMetaData)
.apply(CoGroupByKey.create())
val enrichedData = joinedPipeline.apply(
ParDo.of(object : DoFn<KV<String, CoGbkResult>, ClickHouseModel>() {
#ProcessElement
fun processElement(
#Element data: KV<String, CoGbkResult>,
out: OutputReceiver<ClickHouseModel>
) {
val name = data.key
val source = data.value.getAll(sourceTag)
val userMetadataSource = data.value.getAll(userMetadataTag)
println("==========================")
for (metadata in userMetadataSource.iterator()) {
println("Metadata:: $metadata") // This is always empty
}
for (event in source.iterator()) {
println("Event:: $event")
}
println("==========================")
val sourceEvent = source.iterator().next()
if (userMetadataSource.iterator().hasNext()) {
val userMetadataEvent = userMetadataSource.iterator().next()
out.output(
ClickHouseModel(
eventType = sourceEvent.eventType,
userMetadata = userMetadataEvent
)
)
}
}
})
)
val clickhouseData = enrichedData.apply(
ParDo.of(object : DoFn<ClickHouseModel, Row>() {
#ProcessElement
fun processElement(context: ProcessContext) {
val data = context.element()
context.output(
data.toSchema()
)
}
})
)
return clickhouseData
.setRowSchema(ClickHouseModel.schemaType())
.apply(
ClickHouseIO.write(
"jdbc:clickhouse://127.0.0.1:8123/test?password=example",
"clickhouse_test"
)
)
}
}
UserMetadataEnricher
class UserMetadataEnricher(private val options: MainPipelineOptions) :
PTransform<PCollection<String>, PCollection<UserMetadata>>() {
#Throws(Exception::class)
override fun expand(events: PCollection<String>): PCollection<UserMetadata> {
return events
.apply(
JdbcIO.readAll<String, UserMetadata>()
.withDataSourceConfiguration(
JdbcIO.DataSourceConfiguration.create(
"com.mysql.cj.jdbc.Driver", "jdbc:mysql://localhost:3306/beam-test"
)
.withUsername("root")
.withPassword("example")
)
.withQuery("select id,name,gender from user where id = ?")
.withParameterSetter { id: String, preparedStatement: PreparedStatement ->
preparedStatement.setString(1, id)
}
.withCoder(
SerializableCoder.of(
UserMetadata::class.java
)
)
.withRowMapper
{
println("RowMapper:: ${it.getString(1)}") // printed!!
UserMetadata(
it.getString(1),
it.getString(2),
it.getString(3)
)
}
)
}
}
output
RowMapper:: test-02
RowMapper:: test-01
==========================
Event:: DetailViewEvent(...)
==========================
==========================
Event:: DetailViewEvent(...)
==========================
Update 1 (GlobalWindow to FixedWindow)
using AfterProcessing
I've changed my window setting and added print to SimpleFunction that is assigned to userIdDetailViewEvents.
Window.into<FoodDetailViewEvent>(FixedWindows.of(Duration.standardSeconds(30)))
.triggering(
Repeatedly.forever(
AfterProcessingTime.pastFirstElementInPane().plusDelayOf(
Duration.standardSeconds(1)
)
)
)
.withAllowedLateness(Duration.ZERO)
.discardingFiredPanes()
)
And It prints:
userIdDetailViewEvents Called
userIdDetailViewEvents Called
RowMapper:: test-02
RowMapper:: test-01
==========================
Event:: DetailViewEvent(...)
==========================
==========================
Event:: DetailViewEvent(...)
==========================
Using AfterWatermark
Window.into<FoodDetailViewEvent>(FixedWindows.of(Duration.standardSeconds(30)))
.triggering(
Repeatedly.forever(
AfterWatermark.pastEndOfWindow()
)
)
.withAllowedLateness(Duration.ZERO)
.discardingFiredPanes()
outputs
userIdDetailViewEvents Called
userIdDetailViewEvents Called
RowMapper:: test-02
RowMapper:: test-01
I think using AfterWatermark is correct but It is hanging on somewhere... I guess It's JdbcIO
You are correct, the standard configuration doesn't return results with unbounded collections.
As long as you have a functional window / trigger combination the trick is to set
.withOutputParallelization(false); to the JdbcIO.<>readAll() call as follows:
p.apply("Read from JDBC", JdbcIO.<>readAll()
.withDataSourceConfiguration(getConfiguration())
.withQuery(getStreamingQuery())
.withParameterSetter((JdbcIO.PreparedStatementSetter<String>) (element, preparedStatement) -> {
// Prepare statement here.
})
.withRowMapper((JdbcIO.RowMapper<>) results -> {
// Map results here.
}).withOutputParallelization(false));
I struggled with this for hours but came across the code in this article and it finally worked.
I used
Window.into(new GlobalWindows()).triggering(
Repeatedly.forever(AfterPane.elementCountAtLeast(1))
)
.discardingFiredPanes())
for my trigger to process one element at a time.
GlobalWindow never closes so it's not a suitable option for unbounded data source like pubsub.
I would recommend using FixedWindow(<Some time range>) instead.
You can read more about windows here https://beam.apache.org/documentation/programming-guide/#windowing

Upgrading some Corda3 source code to run on v4

First of all, I've only started learning corda 3 months ago so I've got some learning to do.
I've inherited some code that runs fine under Corda v3.3 but the customers want it to run on v4. I'm trying to follow the instructions on the main website. I've got an initiating flow which calls a subflow, which in turn calls a transponder flow.
The initiating flow:
#InitiatingFlow(version = 2)
#StartableByRPC
class TransferFlow(private val issuerName: String = "",
private val seller: String = "",
private val amount: BigDecimal = BigDecimal("0"),
private val buyer: String = "",
private val custodianNameOfBuyer: String = "",
private val notaryName: String = "") : FlowLogic<SignedTransaction>() {
#Suspendable
override fun call(): SignedTransaction {
subFlow(UpdateStatusOfTransferFlow(
sessions,
tokenTransferAgreement.linearId,
"Removed Tokens From Seller"))
}
}
class UpdateStatusOfTransferFlow(
private val sessions: Set<FlowSession>,
private val tokenTransferAgreementID: UniqueIdentifier,
private val newStatus: String) : FlowLogic<SignedTransaction>() {
#Suspendable
override fun call(): SignedTransaction {
sessions.size
val idQueryCriteria = QueryCriteria.LinearStateQueryCriteria(linearId = listOf(tokenTransferAgreementID))
val states = serviceHub.vaultService.queryBy<TokenTransferAgreement>(idQueryCriteria).states
if (states.size != 1) throw FlowException("Can not find a unique state for $tokenTransferAgreementID")
val inputStateAndRef = states.single()
val inputState = inputStateAndRef.state.data
val notary = inputStateAndRef.state.notary
val outputState = inputState.withNewStatus(newStatus)
val cmd = Command(TokenContract.Commands.UpdateStatusOfTransfer(),
inputState.participants.map { it.owningKey })
val txBuilder = TransactionBuilder(notary = notary)
txBuilder.addCommand(cmd)
txBuilder.addInputState(inputStateAndRef)
txBuilder.addOutputState(outputState, TokenContract.ID)
txBuilder.verify(serviceHub)
val ptx = serviceHub.signInitialTransaction(txBuilder)
val sessions2 = (inputState.participants.toSet() - ourIdentity).map { initiateFlow(it) }
return subFlow(CollectSignaturesFlow(ptx, sessions2))
}
}
And the responder:
#InitiatedBy(TransferFlowResponder::class)
class UpdateStatusOfTransferFlowResponder(private val session: FlowSession) : FlowLogic<Unit>() {
#Suspendable
override fun call() {
val tokenTransferAgreements = mutableListOf<TokenTransferAgreement>()
var isBuyer = true
var notary = CordaUtility.getNotary(serviceHub) ?: throw FlowException("An notary is expected!")
val signedTransactionFlow = subFlow(object : SignTransactionFlow(session) {
override fun checkTransaction(stx: SignedTransaction) = requireThat {
"There must be one output!" using (stx.tx.outputStates.size == 1)
val tokenTransferAgreement = stx.tx.outputStates.first() as TokenTransferAgreement
tokenTransferAgreements.add(tokenTransferAgreement)
notary = stx.notary ?: throw FlowException("An notary is expected!")
if (ourIdentity == tokenTransferAgreement.issuer) {
//checks go here
}
})
}
}
I believe I am supposed to add a call to ReceiveFinality flow at some point, however it only takes 1 session as an argument, not a list as I have here. Should I make multiple calls, one for each session? I am also not sure if the calls should go in the transponder or the UpdateStatusOfTransferFlow class.
Help here would be appreciated.
The FinalityFlow is mainly responsible for ensuring transactions are notarized, distributed accordingly and persisted to local vaults.
In previous versions of Corda, all nodes would by default accept incoming requests for finality.
From V4 onwards, you're required to write a ReceiveFinalityFlow to write your own processing logic before finality.
The way finality currently runs in Corda is the initiating node, as an intermediate step during finality, distributes notarised transaction to all other participants. Each of the participating nodes it sends to will only expect to receive a session from this node.
So where you might submit multiple sessions to the initiating FinalityFlow to include all the participants, the responding nodes will only ever receive just the one session from the initiator.
In the future, we may look at having the Notary distribute the notarized transaction to all participants, but even then, the ReceiveFinalityFlow would still only expect one session, this time from the Notary.