Kafka Exception: The group member needs to have a valid member id before actually entering a consumer group - kotlin

I have this consumer that appears to be connected to the kafka topic but after seeing the logs I'm seeing that the consumer Join group failed with org.apache.kafka.common.errors.MemberIdRequiredException: The group member needs to have a valid member id before actually entering a consumer group. I've tried looking at a few other setups online and I don't see anyone explicitly setting a member ID. I don't see this exception thrown locally so I'm hoping that maybe I just need to do some fine-tuning on the consumer configurations but I'm not sure what needs to be changed.
Kafka Consumer
#KafkaListener(
topics = ["my.topic"],
groupId ="kafka-consumer-group"
)
fun consume(consumerRecord: ConsumerRecord<String, Record>, ack: Acknowledgment) {
val value = consumerRecord.value()
val eventKey = consumerRecord.key()
val productNotificationEvent = buildProductNotificationEvent(value)
val consumerIdsByEventKey = eventKey.let { favoritesRepository.getConsumerIdsByEntityId(it) }
ack.acknowledge()
}
Kafka Consumer Config
#EnableKafka
#Configuration
class KafkaConsumerConfig(
#Value("\${KAFKA_SERVER}")
private val kafkaServer: String,
#Value("\${KAFKA_SASL_USERNAME}")
private val userName: String,
#Value("\${KAFKA_SASL_PASSWORD}")
private val password: String,
#Value("\${KAFKA_SCHEMA_REGISTRY_URL}")
private val schemaRegistryUrl: String,
#Value("\${KAFKA_SCHEMA_REGISTRY_USER_INFO}")
private val schemaRegistryUserInfo: String,
#Value("\${KAFKA_CONSUMER_MAX_POLL_IN_MS}")
private val maxPollIntervalMsConfig: Int,
#Value("\${KAFKA_CONSUMER_MAX_POLL_RECORDS}")
private val maxPollRecords: Int
) {
#Bean
fun consumerFactory(): ConsumerFactory<String?, Any?> {
val props: MutableMap<String, Any> = mutableMapOf()
props[ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG] = kafkaServer
props[ConsumerConfig.GROUP_ID_CONFIG] = KAFKA_CONSUMER_GROUP_ID
props[ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java
props[ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG] = KafkaAvroDeserializer::class.java
props[ConsumerConfig.AUTO_OFFSET_RESET_CONFIG] = "earliest"
props[ConsumerConfig.MAX_POLL_RECORDS_CONFIG] = maxPollRecords
props[ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG] = maxPollIntervalMsConfig
props[CommonClientConfigs.SECURITY_PROTOCOL_CONFIG] = "SASL_SSL"
props[SaslConfigs.SASL_MECHANISM] = "PLAIN"
val module = "org.apache.kafka.common.security.plain.PlainLoginModule"
val jaasConfig = String.format(
"%s required username=\"%s\" password=\"%s\";",
module,
userName,
password
)
props[SaslConfigs.SASL_JAAS_CONFIG] = jaasConfig
props[KafkaAvroDeserializerConfig.VALUE_SUBJECT_NAME_STRATEGY] = TopicRecordNameStrategy::class.java
props[KafkaAvroDeserializerConfig.SCHEMA_REGISTRY_URL_CONFIG] = schemaRegistryUrl
props[KafkaAvroDeserializerConfig.BASIC_AUTH_CREDENTIALS_SOURCE] = "USER_INFO"
props[KafkaAvroDeserializerConfig.USER_INFO_CONFIG] = schemaRegistryUserInfo
return DefaultKafkaConsumerFactory(props)
}
#Bean
fun kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, Any>? {
val factory = ConcurrentKafkaListenerContainerFactory<String, Any>()
factory.consumerFactory = consumerFactory()
factory.containerProperties.ackMode = ContainerProperties.AckMode.MANUAL_IMMEDIATE
factory.containerProperties.isSyncCommits = true
return factory
}
companion object {
private const val KAFKA_CONSUMER_GROUP_ID = "kafka-consumer-group"
}
}

Related

How to test a service function in kotlin which uses a late init Id?

I am new to testing in kotlin and I was wondering how I can test this function:
this is my SegmentService file:
fun createSegmentFromUserIds(userIds: List<String>, name:String, appId: String): Segmentation {
val filter = createUserIdFilter(userIds)
val createSegmentRequest = CreateSegmentRequest(
name = name, attribute = filter, type = SegmentType.STATIC
)
val segmentation = segmentMetaService.saveSegmentInfo(createSegmentRequest, appId)
querySegmentService.runUpdateQuery(users = userIds, appId = appId, segmentId = segmentation.id)
return segmentation
}
this is the saveSegmentInfo function:
fun saveSegmentInfo(createSegmentFilter: CreateSegmentRequest, appId: String): Segmentation {
val segmentInfo = segmentationRepository.save(createSegmentFilter.let {
Segmentation(
attribute = it.attribute, name = it.name, appId = appId, type = it.type
)
})
logger.info("Segment info saved with id: ${segmentInfo.id}, name: ${segmentInfo.name}")
return segmentInfo
}
and this is the Segmentation Document
#Document(Segmentation.COLLECTION_NAME)
class Segmentation(
#Field(ATTRIBUTE)
val attribute: Filter,
#Field(NAME)
val name: String,
#Indexed
#Field(APP_ID)
val appId: String,
#Field(STATUS)
var status: SegmentStatus = SegmentStatus.CREATED,
#Field(TYPE)
var type: SegmentType = SegmentType.STATIC,
#Field(USER_COUNT)
var userCount: Long? = null,
) {
#Id
lateinit var id: String
#Field(CREATION_DATE)
var creationDate: Date = Date()
}
I have written this test for it:
class SegmentServiceTest {
private val segmentMetaService = mock(SegmentMetaService::class.java)
private val querySegmentService = mock(QuerySegmentService::class.java)
private val converterService = mock(ConverterService::class.java)
private val userAttributeService = mock(UserAttributeService::class.java)
private val segmentConsumerUserInfoProducer = mock(SegmentConsumerUsersInfoProducer::class.java)
private val segmentationRepository = mock(SegmentationRepository::class.java)
#Test
fun `createSegmentFromUserIds should create a new segment`() {
val segmentService = SegmentService(
segmentMetaService = segmentMetaService,
querySegmentService = querySegmentService,
converterService = converterService,
userAttributeService = userAttributeService,
segmentConsumerUserInfoProducer = segmentConsumerUserInfoProducer
)
val userIds = listOf("user-1", "user-2", "user-3")
val filter = AndFilter(
operations = listOf(
AndFilter(
operations = listOf(
StringListContainsFilter(
field = "userId", operationType = StringQueryOperationType.IN, values = userIds
)
)
)
)
)
val createSegmentRequest = CreateSegmentRequest(
name = "segment-name", attribute = filter, type = SegmentType.STATIC
)
val segment = Segmentation(attribute = filter, name = "segment-name", type = SegmentType.STATIC, appId = "app-id" )
`when`(segmentationRepository.save(any())).thenReturn(segment)
`when`(segmentMetaService.saveSegmentInfo(createSegmentRequest, "app-id")).thenReturn(segment)
val createdSegment = segmentService.createSegmentFromUserIds(userIds = userIds, name = "segment-name", appId = "app-id")
assertEquals(segment, createdSegment)
}
}
but I am facing this error:
kotlin.UninitializedPropertyAccessException: lateinit property id has not been initialized
So what am I doing wrong here?
How can I initialize the Id? or should I refactor my code so for it to become testable?
I think you are mocking / providing the answers to the wrong calls.
you mock segmentationRepository.save(..) but this call won't ever be made since that call is inside segmentMetaService.saveSegmentInfo(...) which is mocked so the real code is not called.
when you call segmentService.createSegmentFromUserIds(..), my guess (stack trace would be useful on the error, BTW), is that this method proceeds passed the invocation of segmentMetaService.saveSegmentInfo(...) but in the next line you call querySegmentService.runUpdateQuery(users = userIds, appId = appId, segmentId = segmentation.id) which is accessing the uninitialised id.
The fix will be to set the segment.id when you set up the object for the answer to segmentMetaService.saveSegmentInfo(..)

FOREIGN KEY constraint failed (code 787 SQLITE_CONSTRAINT_FOREIGNKEY) when add data to table room

I have to tables CheckListModel and CheckListPoints, it is one to n relationship, when i try to add
data in DB CheckListModel adds correctly, but when code gose to add CheckListPoints i got this error. I have no idea why this happening
This is my DB
#Database(entities = [CheckListModel::class,CheckListPoints::class],version = 4,exportSchema = false)
abstract class CheckListDB : RoomDatabase() {
abstract fun checkListDBDao():CheckListModelDBDao
companion object {
#Volatile
private var instance: CheckListDB? = null
fun getInstance(context: Context):CheckListDB{
return instance ?: synchronized(this){
instance?: buildDatabase(context).also { instance = it }
}
}
private fun buildDatabase(context: Context): CheckListDB{
return Room.databaseBuilder(context,CheckListDB::class.java,"check_list_model").fallbackToDestructiveMigration().build()
}
}
}
entityes
#Entity(
tableName = "check_list_point",
foreignKeys = [
ForeignKey(entity = CheckListModel::class, parentColumns = ["checkListModelID"],childColumns = ["checkListColumnID"],onDelete = ForeignKey.CASCADE)
],
indices = [Index("checkListColumnID")]
)
data class CheckListPoints(
#ColumnInfo(name = "correctly")
var correctly: Boolean,
#ColumnInfo(name = "requirement")
var requirement: String,
#ColumnInfo(name = "passed")
var passed: Boolean,
#ColumnInfo(name="checkListColumnID")
val checkListColumnID: Long,
#PrimaryKey(autoGenerate = true)
val checkListPointsModelID: Long = 0L
): Serializable
#Entity(tableName = "check_list_model")
data class CheckListModel (
#ColumnInfo(name = "check_list_name")
val checkListName: String,
#ColumnInfo(name = "check_list_count")
val checkListCount: Int,
#ColumnInfo(name = "check_list_result")
val checkListResult: Int,
#ColumnInfo(name = "description")
val description: String,
#PrimaryKey(autoGenerate = true)
val checkListModelID: Long = 0L
) : Serializable
relationship
data class CheckListWithCheckListModel(
#Embedded val CheckList: CheckListModel,
#Relation(
parentColumn = "checkListModelID",
entityColumn = "checkListColumnID"
)
val checkListPoints: List<CheckListPoints>
)
this is dao
#Dao
interface CheckListModelDBDao {
#Insert
fun insertCheckList(data:CheckListModel)
#Insert
fun insertCheckListPoint(vararg data:CheckListPoints)
#Delete
fun deleteCheckList(checkList: CheckListModel)
#Transaction
#Query("SELECT * FROM check_list_model " )
fun getEverything(): Flow<List<CheckListWithCheckListModel>>
}
and this is how i add
private var doorCheckListModel = CheckListModel("Дверь",0,0,"4321")
private val doorCheckListPoint1 = CheckListPoints(false,"1",false,doorCheckListModel.checkListModelID)
private val doorCheckListPoint2 = CheckListPoints(false,"2",false,doorCheckListModel.checkListModelID)
private var doorListOfCheckListPoints = listOf<CheckListPoints>(doorCheckListPoint1,doorCheckListPoint2)
private var windowCheckListModel = CheckListModel("Окно",0,0,"4321")
private var windowCheckListPoint1 = CheckListPoints(false,"1",false,windowCheckListModel.checkListModelID)
private var windowCheckListPoint2 = CheckListPoints(false,"1",false,windowCheckListModel.checkListModelID)
private var windowListOfCheckListPoints = listOf<CheckListPoints>(windowCheckListPoint1,windowCheckListPoint2)
var checkLists = MutableLiveData<List<CheckListModel>>().apply {
value = listOf(doorCheckListModel,windowCheckListModel)
}
fun addCheckList(name: String){
viewModelScope.launch(Dispatchers.IO) {
when (name) {
"Дверь" -> insert(doorCheckListModel,doorListOfCheckListPoints)
"Окно" -> insert(windowCheckListModel,windowListOfCheckListPoints)
}
}
}
private suspend fun insert(checkList: CheckListModel, checkListPoints: List<CheckListPoints>){
database.insertCheckList(checkList)
for(checkListPoint in checkListPoints){
database.insertCheckListPoint(checkListPoint)
}
}
}
also i display data from CheckListModel in fragment. CheckListModel added to DB Correctly and display correctly, but CheckListPoints has not
When you create doorCheckListModel, its checkListModelID is initially 0. You use this 0 as checkListColumnID in doorCheckListPoint1. So when you save the CheckListModel, Room automatically generates the primary key and saves in the table. Similar is the case for primary key in CheckListPoints table. But the entries saved in CheckListPoints table still have 0 in checkListColumnID column.
This is why the foreign key constraint is failing. There is no CheckListModel with 0 as its primary key. To fix this, you will have to set the value of checkListColumnID before saving a CheckListPoints entry in the table.
If you go through Room documentation, the #Insert annotated function can optionally return the rowId for the inserted item. For integer primary keys, rowId is the same as primary key.
Try this code:
// Return the primary key here
#Insert
fun insertCheckList(data:CheckListModel): Long
private suspend fun insert(checkList: CheckListModel, checkListPoints: List<CheckListPoints>){
val id = database.insertCheckList(checkList)
for(checkListPoint in checkListPoints){
database.insertCheckListPoint(checkListPoint.copy(checkListColumnID = id))
}
}

net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException: Could not create the DataSource

I'm trying to develop a CorDapp using the example here. I've added two modules into my project, one for contracts and the other one for flows. I've added test cases for my contract and it works fine, but test cases for flow fail on setup stage. Here's the code of my test class
#TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SharedInformationFlowTests {
private lateinit var network: MockNetwork
private lateinit var a: StartedMockNode
private val proposal = LedgerUpdateProposal.testProposal()
#BeforeAll
fun setup() {
val params = MockNetworkParameters(cordappsForAllNodes = listOf(
TestCordapp.findCordapp("com.something.contract")
))
network = MockNetwork(params) //fails here
a = network.createPartyNode()
network.runNetwork()
}
#AfterAll
fun tearDown() {
network.stopNodes()
}
And here are the error messages I get:
[WARN] 13:42:52,620 [main] spi.SqlExceptionHelper. - SQL Error: 0, SQLState: null {changeSet=migration/vault-schema.changelog-v9.xml::update-vault-states::R3.Corda, databaseChangeLog=master.changelog.json}
[ERROR] 13:42:52,620 [main] spi.SqlExceptionHelper. - Connection is closed {changeSet=migration/vault-schema.changelog-v9.xml::update-vault-states::R3.Corda, databaseChangeLog=master.changelog.json}
net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException: Could not create the
DataSource: Migration failed for change set migration/vault-schema.changelog-v9.xml::update-vault-states::R3.Corda:
Reason: net.corda.nodeapi.internal.persistence.HibernateConfigException: Could not create Hibernate configuration: Unable to open JDBC Connection for DDL execution
I think there's something wrong with the liquibase. I've tried adding changelog file to my resources/migration directory as advised here, but it doesn't seem to have any effect. Please help.
UPDATE Added schema
/**
* The family of com.sentinel.schemas for SharingInformationState.
*/
object SharingInformationSchema
/**
* An SharingInformationState schema.
*/
object SharingInformationSchemaV1 : MappedSchema(
schemaFamily = SharingInformationSchema.javaClass,
version = 1,
mappedTypes = listOf(PersistentSharingInformation::class.java)) {
override val migrationResource: String? = "sharing-information-schema-v1.changelog-master.xml"
#Entity
#Table(name = "persistent_sharing_information")
class PersistentSharingInformation(
#Column(name = "owner_id")
var dataOwnerId: Long,
#Column(name = "buyer_id")
var dataBuyerId: Long,
#Column(name = "start_date")
val startDate: String,
#Column(name = "end_date")
val endDate: String,
#Column(name = "shared_fields")
val sharedFieldsIds: String,
#Column(name = "agreement_status")
val agreementStatus: String,
#Column(name = "contract_type")
val contractType: String,
#Column(name = "linear_id")
var linearId: UUID
) : PersistentState() {
// Default constructor required by hibernate.
constructor() : this(0L, 0L,
"", "", "[]", "", "", UUID.randomUUID())
}
}
#BelongsToContract(com.package.contract.SharingInformationContract::class)
class SharingInformationState(val ourParty: Party,
val proposal: LedgerUpdateProposal,
override val linearId: UniqueIdentifier = UniqueIdentifier()) : LinearState, QueryableState {
override val participants: List<AbstractParty> = listOf(ourParty)
override fun generateMappedObject(schema: MappedSchema): PersistentState {
return when (schema) {
SharingInformationSchemaV1 -> SharingInformationSchemaV1.PersistentSharingInformation(
proposal.ownerId,
proposal.buyerId,
proposal.startDate,
proposal.endDate,
proposal.sharedFieldsIds.toString(),
proposal.agreementStatus.name,
proposal.contractType.name,
linearId.id
)
else -> throw IllegalArgumentException("Unrecognised schema $schema")
}
}
override fun supportedSchemas(): Iterable<MappedSchema> = listOf(SharingInformationSchemaV1)
}
#CordaSerializable
enum class AgreementStatus { APPROVED, REJECTED }
#CordaSerializable
enum class ContractType { CORPORATE, CONSUMER, MARKETING, BINDING }

Make na abstraction to kafka consumer using ktor and kotlin

I'm creating a abstraction for consumer and producer Kafka, to avoid duplicate code all the time. So i created a lib using kotlin and gradle, named "kafka-commons", and put the following code:
For Kafka producer:
fun producer(
bootstrapServers: String,
idempotence: Boolean,
acks: Acks,
retries: Int,
requestPerConnection: Int,
compression: Compression,
linger: Int,
batchSize: BatchSize
): KafkaProducer<String, Any> {
val prop: HashMap<String, Any> = HashMap()
prop[BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers
prop[KEY_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java.name
prop[VALUE_SERIALIZER_CLASS_CONFIG] = StringSerializer::class.java.name
prop[ENABLE_IDEMPOTENCE_CONFIG] = idempotence
prop[ACKS_CONFIG] = acks.value
prop[RETRIES_CONFIG] = retries
prop[MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION] = requestPerConnection
prop[COMPRESSION_TYPE_CONFIG] = compression.value
prop[LINGER_MS_CONFIG] = linger
prop[BATCH_SIZE_CONFIG] = batchSize.value
return KafkaProducer(prop)
}
suspend inline fun <reified K : Any, reified V : Any> KafkaProducer<K, V>.dispatch(record: ProducerRecord<K, V>) =
suspendCoroutine<RecordMetadata> { continuation ->
val callback = Callback { metadata, exception ->
if (metadata == null) {
continuation.resumeWithException(exception!!)
} else {
continuation.resume(metadata)
}
}
this.send(record, callback)
}
So, i created a default Command object with the following structure
data class Command(
val id: UUID,
val status: CommandStatus,
val message: Any
)
id: create a unique ID
status: crete a message status (can be: Open /Processing / Closed / Error)
message: an object from http reques (for example: If have an Post
For example: if have a "insert user: POST" with body :
{ "id": 1, "name" : "John", "lastName" : "Wick" }
so message will be this object, and so on.
And for create this command, i made this function:
suspend fun creatCommand(
topicName: String,
id: UUID,
commandStatus: CommandStatus,
request: Any,
bootstrapServers: String,
idempotence: Boolean,
acks: Acks,
retries: Int,
requestPerConnection: Int,
compression: Compression,
linger: Int,
batchSize: BatchSize
): Unit {
val producer = producer(
bootstrapServers,
idempotence,
acks,
retries,
requestPerConnection,
compression,
linger,
batchSize)
val command = toCommand(processStarted(id, commandStatus, request))
val record = ProducerRecord<String, Any>(topicName, id.toString(), command)
coroutineScope { launch { producer.dispatch(record) } }
}
So, I created other APIs and just call this function to create a producer that sends a command to kafka. Like:
fun Route.user(service: Service) =
route("/api/access") {
post("/test") {
call.respond(service.command(call.receive()))
}
}
>>>>>> other class <<<<<<<<
classService () {
fun command( all parameters) { creatCommand(all parameters)}
}
Sor far so good. All works great.
Now my problemas begin. I'm trying to create a consumer.
First i made this:
fun consumer(
bootstrapServers: String,
group: String,
autoCommit: Boolean,
offsetBehaviour: OffsetBehaviour,
pollMax: Int
): KafkaConsumer<String, Any> {
val prop: HashMap<String, Any> = HashMap()
prop[BOOTSTRAP_SERVERS_CONFIG] = bootstrapServers
prop[KEY_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java.name
prop[VALUE_DESERIALIZER_CLASS_CONFIG] = StringDeserializer::class.java.name
prop[GROUP_ID_CONFIG] = group
prop[AUTO_OFFSET_RESET_CONFIG] = offsetBehaviour
prop[ENABLE_AUTO_COMMIT_CONFIG] = autoCommit
prop[MAX_POLL_RECORDS_CONFIG] = pollMax
return KafkaConsumer(prop)
}
And after:
fun<T> recordingCommand(
command: Class<T>,
topic: String,
bootstrapServers: String,
group: String,
autoCommit: Boolean,
offsetBehaviour: OffsetBehaviour,
pollMax: Int
) {
val consumer = consumer(bootstrapServers, group, autoCommit, offsetBehaviour, pollMax)
consumer.subscribe(mutableListOf(topic))
while (true) {
val records = consumer.poll(Duration.ofMillis(100))
for (record in records) {
val om = ObjectMaper
om.readvalue(record.value(), command::class.java)
>>>> I GOT LOST HERE <<<<<<
}
}
}
What i need: creating a abstract consumer that record all data inside a Command.message() (only the message) in a database.
For example, i need to record the user above (id 1, john wick ) into a postgresql database.
So if i had a service with insert method, i can call it, passing insertmethod like:
service.insert(recordingCommand(all parameters)).
Anyone can help me with that?
If I understood correctly, you are having issues with JSON mapping into objects
val mapper = ObjectMaper
while (true) {
val records = consumer.poll(Duration.ofMillis(100))
for (record in records) {
val cmd = mapper.readvalue(record.value(), command::class.java)
// do things with cmd
}
}
Note: Kafka has its own JSON to POJO deserializer, and if you want to send data to a database, Kafka Connect is generally more fault-tolerant than your simple consume loop.

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.