diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/domain/Event.kt b/p8e-api/src/main/kotlin/io/provenance/engine/domain/Event.kt index 292d4f12..7c683c43 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/domain/Event.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/domain/Event.kt @@ -29,33 +29,21 @@ object EventTable : UUIDTable(name = "event", columnName = "uuid") { open class EventEntityClass : UUIDEntityClass(EventTable) { fun findForUpdate(uuid: UUID) = find { EventTable.id eq uuid }.forUpdate().firstOrNull() - fun insertOrUpdate( + fun insert( event: P8eEvent, envelopeUuid: UUID, status: EventStatus = EventStatus.CREATED, created: OffsetDateTime = OffsetDateTime.now(), updated: OffsetDateTime = OffsetDateTime.now() - ): EventRecord = findByEnvelopeUuidForUpdate(envelopeUuid)?.also { - it.event = event.event - it.payload = event - it.status = status - it.created = created - it.updated = updated - } ?: new(UUID.randomUUID()) { - this.event = event.event - this.payload = event - this.status = status - this.envelopeUuid = envelopeUuid - this.created = created - this.updated = updated + ): EventRecord = new(UUID.randomUUID()) { + this.event = event.event + this.payload = event + this.status = status + this.envelopeUuid = envelopeUuid + this.created = created + this.updated = updated } - - fun findByEvent(event: P8eEvent.Event): List = - find{ - (EventTable.event eq event) - }.toList() - fun findForConnectedClients(where: (SqlExpressionBuilder.()-> Op)) = EventTable .innerJoin(EnvelopeTable) .join(AffiliateConnectionTable, JoinType.INNER, EnvelopeTable.contractClassname, AffiliateConnectionTable.classname) { @@ -77,6 +65,19 @@ class EventRecord(uuid: EntityID) : UUIDEntity(uuid) { var status by EventTable.status var created by EventTable.created var updated by EventTable.updated + + fun update( + event: P8eEvent, + status: EventStatus = EventStatus.CREATED, + created: OffsetDateTime = OffsetDateTime.now(), + updated: OffsetDateTime = OffsetDateTime.now() + ) = apply { + this.event = event.event + this.payload = event + this.status = status + this.created = created + this.updated = updated + } } enum class EventStatus { diff --git a/p8e-api/src/main/kotlin/io/provenance/engine/service/EventService.kt b/p8e-api/src/main/kotlin/io/provenance/engine/service/EventService.kt index 0dc24f7c..698bb787 100644 --- a/p8e-api/src/main/kotlin/io/provenance/engine/service/EventService.kt +++ b/p8e-api/src/main/kotlin/io/provenance/engine/service/EventService.kt @@ -38,6 +38,16 @@ class EventService() { val SKIPPABLE_EVENTS = listOf(Event.UNRECOGNIZED, Event.SCOPE_INDEX_FRAGMENT) val CONNECTED_CLIENT_EVENTS = listOf(Event.ENVELOPE_REQUEST, Event.ENVELOPE_RESPONSE, Event.ENVELOPE_ERROR) val UNCONNECTED_CLIENT_EVENTS = Event.values().toList().minus(CONNECTED_CLIENT_EVENTS).minus(SKIPPABLE_EVENTS) + + val VALID_STATE_TRANSITIONS = mapOf( + Event.ENVELOPE_FRAGMENT to listOf(Event.ENVELOPE_CHAINCODE, Event.ENVELOPE_ERROR), + Event.ENVELOPE_REQUEST to listOf(Event.ENVELOPE_MAILBOX_OUTBOUND), + Event.ENVELOPE_MAILBOX_OUTBOUND to listOf(Event.SCOPE_INDEX, Event.SCOPE_INDEX_FRAGMENT), + Event.ENVELOPE_CHAINCODE to listOf(Event.SCOPE_INDEX, Event.SCOPE_INDEX_FRAGMENT, Event.ENVELOPE_ERROR), + Event.SCOPE_INDEX to listOf(Event.ENVELOPE_RESPONSE), + Event.SCOPE_INDEX_FRAGMENT to listOf(Event.ENVELOPE_RESPONSE), + Event.ENVELOPE_RESPONSE to listOf(), // end of the line + ) } fun registerCallback(event: Event, callback: EventHandler) { @@ -49,7 +59,13 @@ class EventService() { } fun submitEvent(event: P8eEvent, envelopeUuid: UUID, status: EventStatus = EventStatus.CREATED, created: OffsetDateTime = OffsetDateTime.now()): EventRecord = - EventRecord.insertOrUpdate(event, envelopeUuid, status, created, created).also(::submitToChannel) + EventRecord.findByEnvelopeUuidForUpdate(envelopeUuid) + ?.also { + val validTransitions = VALID_STATE_TRANSITIONS[it.event] + if (validTransitions == null || validTransitions.contains(event.event)) { + it.update(event, status, created, created).also(::submitToChannel) + } + } ?: EventRecord.insert(event, envelopeUuid, status, created, created).also(::submitToChannel) fun completeInProgressEvent(envelopeUuid: UUID, expectedEventType: Event) = EventRecord.findByEnvelopeUuidForUpdate(envelopeUuid) ?.takeIf { it.event == expectedEventType } diff --git a/p8e-api/src/test/kotlin/service/EventServiceTest.kt b/p8e-api/src/test/kotlin/service/EventServiceTest.kt index 27f68190..9ac9dddd 100644 --- a/p8e-api/src/test/kotlin/service/EventServiceTest.kt +++ b/p8e-api/src/test/kotlin/service/EventServiceTest.kt @@ -52,11 +52,6 @@ class EventServiceTest { val key = TestUtils.generateKeyPair() - val eventMock = Events.P8eEvent.newBuilder() - .setEvent(Event.ENVELOPE_REQUEST) - .setMessage(key.public.toHex().toByteString()) - .build() - val testData = ContractScope.EnvelopeState.newBuilder() .setContractClassname("HelloWorldContract") .build() @@ -92,15 +87,6 @@ class EventServiceTest { status = ContractScope.Envelope.Status.CREATED scopeUuid = scopeRecord.uuid } - - //Insert data into EventTable - EventTable.insert { - it[payload] = eventMock - it[status] = EventStatus.CREATED - it[event] = eventMock.event - it[envelopeUuid] = envelopeRecord.uuid.value - it[created] = createdTime - } } eventService = EventService() @@ -111,6 +97,13 @@ class EventServiceTest { @Test fun `Test in-progress events are updated to completed`() { transaction { + val event = Events.P8eEvent.newBuilder() + .setEvent(Event.ENVELOPE_REQUEST) + .setMessage("some-test-message".toByteString()) + .build() + + EventRecord.insert(event, envelopeRecord.uuid.value) + //Execute eventService.completeInProgressEvent(envelopeRecord.uuid.value, Event.ENVELOPE_REQUEST) @@ -186,4 +179,44 @@ class EventServiceTest { Assert.assertEquals(EventStatus.CREATED, testErrorEventRecord.status) Assert.assertEquals(envelopeRecord.uuid.value, testErrorEventRecord.envelopeUuid) } + + @Test + fun `Verify event submission skipped for invalid transition`() { + val event = Events.P8eEvent.newBuilder() + .setEvent(Event.ENVELOPE_RESPONSE) + .setMessage("some-test-message".toByteString()) + .build() + + transaction { EventRecord.insert(event, envelopeRecord.uuid.value) } + + val event2 = Events.P8eEvent.newBuilder() + .setEvent(Event.SCOPE_INDEX) + .setMessage("some-other-test-message".toByteString()) + .build() + val updatedRecord = transaction { eventService.submitEvent(event2, envelopeRecord.uuid.value, EventStatus.CREATED, createdTime) } + + // event should remain unchanged, since you can't go from ENVELOPE_RESPONSE -> SCOPE_INDEX + Assert.assertEquals(Event.ENVELOPE_RESPONSE, updatedRecord.event) + Assert.assertEquals("some-test-message".toByteString(), updatedRecord.payload.message) + } + + @Test + fun `Verify event submission works for a valid transition`() { + val event = Events.P8eEvent.newBuilder() + .setEvent(Event.SCOPE_INDEX) + .setMessage("some-test-message".toByteString()) + .build() + + transaction { EventRecord.insert(event, envelopeRecord.uuid.value) } + + val event2 = Events.P8eEvent.newBuilder() + .setEvent(Event.ENVELOPE_RESPONSE) + .setMessage("some-other-test-message".toByteString()) + .build() + val updatedRecord = transaction { eventService.submitEvent(event2, envelopeRecord.uuid.value, EventStatus.CREATED, createdTime) } + + // event should be updated, since you can go from SCOPE_INDEX -> ENVELOPE_RESPONSE + Assert.assertEquals(Event.ENVELOPE_RESPONSE, updatedRecord.event) + Assert.assertEquals("some-other-test-message".toByteString(), updatedRecord.payload.message) + } }