Skip to content

Commit

Permalink
chore: allow null values for groupId
Browse files Browse the repository at this point in the history
Repurpose the `groupId` semantics to utilize null values for indicating
items that are not part of a group. Accordingly, the default group id
provider is updated to simply return `null`.

The change was motivated by three points:
- A `null` value is semantically closer to the notion of
  'not part of a group'.
- Make it easier for applications to upgrade to the new library since
  they don't need to run a migration against existing items.
- It allows quick and easy identification of whether an item might
  belong to a group or not. Implementing application could potentially
  use this information to decide whether they need to retrieve
  additional items or not and therefore improve the fetching
  performance.

[SW-580]
  • Loading branch information
panos-tr committed Sep 20, 2024
1 parent 5a05cc2 commit 455888b
Show file tree
Hide file tree
Showing 8 changed files with 32 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import io.github.bluegroundltd.outbox.executor.FixedThreadPoolExecutorServiceFac
import io.github.bluegroundltd.outbox.grouping.DefaultGroupingConfiguration
import io.github.bluegroundltd.outbox.grouping.OutboxGroupingConfiguration
import io.github.bluegroundltd.outbox.grouping.OutboxGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.RandomGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.NullGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.SingleItemGroupingConfiguration
import io.github.bluegroundltd.outbox.item.OutboxType
import io.github.bluegroundltd.outbox.item.factory.OutboxItemFactory
Expand Down Expand Up @@ -57,7 +57,7 @@ class TransactionalOutboxBuilder(
private lateinit var cleanupLocksProvider: OutboxLocksProvider
private lateinit var store: OutboxStore
private lateinit var instantOutboxPublisher: InstantOutboxPublisher
private var groupIdProvider: OutboxGroupIdProvider = RandomGroupIdProvider()
private var groupIdProvider: OutboxGroupIdProvider = NullGroupIdProvider()
private var groupingConfiguration: OutboxGroupingConfiguration = DefaultGroupingConfiguration

companion object {
Expand Down Expand Up @@ -198,8 +198,8 @@ class TransactionalOutboxBuilder(
/**
* Sets the group id provider for the outbox that will be used to set corresponding field when an item is added.
*
* If not set, a default [OutboxGroupIdProvider] is used that provides a random value, effectively ensuring that
* no groups will be formed.
* If not set, a default [OutboxGroupIdProvider] is used that always returns null, effectively indicating that
* there are no groups.
*/
override fun withGroupIdProvider(groupIdProvider: OutboxGroupIdProvider): BuildStep {
this.groupIdProvider = groupIdProvider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@ package io.github.bluegroundltd.outbox.grouping

import io.github.bluegroundltd.outbox.item.OutboxItem
import io.github.bluegroundltd.outbox.item.OutboxItemGroup
import java.util.UUID

/**
* An [OutboxGroupingProvider] that groups [OutboxItem]s by their group ID.
*
* The provider assumes that all items have a group ID set. Otherwise, an exception will be thrown.
* If an item does not have a group ID set, a temporary random id (UUID) is used to essentially create a
* 'group of one'. This is done (instead e.g. of pre-filtering the items without group id) to ensure that
* the **order of groups** remain consistent with the original item order. The order of items **within a group**
* is determined by the supplied [OutboxOrderingProvider] which defaults to [FifoOrderingProvider].
*/
internal class GroupIdGroupingProvider(
private val orderingProvider: OutboxOrderingProvider = FifoOrderingProvider()
) : OutboxGroupingProvider {
override fun execute(items: Iterable<OutboxItem>) = items
.groupBy { it.groupId!! } // group by group ID
.groupBy { it.groupId ?: UUID.randomUUID() } // group by group ID
.map { orderingProvider.execute(it.value) } // order items in each group
.map { OutboxItemGroup(it) } // create groups
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package io.github.bluegroundltd.outbox.grouping

import io.github.bluegroundltd.outbox.item.OutboxPayload
import io.github.bluegroundltd.outbox.item.OutboxType
import java.util.UUID

/**
* An [OutboxGroupIdProvider] that generates random group IDs.
* An [OutboxGroupIdProvider] that always returns null, indicating that the item is not part of a group.
*/
internal class RandomGroupIdProvider : OutboxGroupIdProvider {
override fun execute(type: OutboxType, payload: OutboxPayload): String = UUID.randomUUID().toString()
internal class NullGroupIdProvider : OutboxGroupIdProvider {
override fun execute(type: OutboxType, payload: OutboxPayload): String? = null
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import io.github.bluegroundltd.outbox.item.OutboxType
* Defines a provider that can be used to generate group IDs for outbox items.
*/
interface OutboxGroupIdProvider {
fun execute(type: OutboxType, payload: OutboxPayload): String
fun execute(type: OutboxType, payload: OutboxPayload): String?
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@ import java.time.Instant
/**
* Represents an outbox item.
*
* @param groupId an arbitrary value that can be used to group outbox items together. The value is defined as nullable
* to allow for backward compatibility with existing outbox items. However, for all intents and purposes, the library
* expects this value to be non-null. At a later stage, the value will be made non-nullable which would introduce a
* breaking change.
* @param groupId an arbitrary value that can be used to group outbox items together. A null value indicates that the
* item is not part of a group.
*
* @property markedForProcessing is an internal flag that is used to determine if the item should be processed in the
* current monitor cycle. It is set by [prepareForProcessing] based on the item status and current time.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,25 @@ class GroupIdGroupingProviderSpec extends Specification {
OutboxItemBuilder.make().withGroupId("group1").build(),
OutboxItemBuilder.make().withGroupId("group2").build(),
OutboxItemBuilder.make().withGroupId("group3").build(),
OutboxItemBuilder.make().withoutGroupId().build(),
OutboxItemBuilder.make().withGroupId("group3").build(),
OutboxItemBuilder.make().withoutGroupId().build(),
OutboxItemBuilder.make().withGroupId("group2").build()
]

and:
def group1 = [items[1]]
def group2 = [items[2], items[5]]
def group3 = [items[0], items[3], items[4]]
// The groups are created in the order of the first appearance of the group ID.
def group2 = [items[2], items[7]]
def group3 = [items[0], items[3], items[5]]
def group4 = [items[4]] // first null group
def group5 = [items[6]] // second null group
// The groups are created in the order of appearance of the group's first item.
def groupedItems = [
group3,
group1,
group2
group2,
group4,
group5
]
def expectedGroups = groupedItems.collect { new OutboxItemGroup(it) }

Expand All @@ -81,20 +87,4 @@ class GroupIdGroupingProviderSpec extends Specification {
and:
groups == []
}

def "Should throw an exception when the group ID is null"() {
given:
def items = (1..3).collect { OutboxItemBuilder.make().build() } +
OutboxItemBuilder.make().withoutGroupId().build()

when:
provider.execute(items)

then:
0 * _

and:
def ex = thrown(NullPointerException)
ex
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import io.github.bluegroundltd.outbox.item.OutboxPayload
import io.github.bluegroundltd.outbox.item.OutboxType
import spock.lang.Specification

class RandomGroupIdProviderSpec extends Specification {
private RandomGroupIdProvider provider = new RandomGroupIdProvider()
class NullGroupIdProviderSpec extends Specification {
private NullGroupIdProvider provider = new NullGroupIdProvider()

def "Should generate a random group id"() {
def "Should return null"() {
given:
def type = GroovyMock(OutboxType)
def payload = Mock(OutboxPayload)
Expand All @@ -19,6 +19,6 @@ class RandomGroupIdProviderSpec extends Specification {
0 * _

and:
groupId
groupId == null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import io.github.bluegroundltd.outbox.grouping.GroupIdGroupingProvider
import io.github.bluegroundltd.outbox.grouping.OutboxGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.OutboxGroupingConfiguration
import io.github.bluegroundltd.outbox.grouping.OutboxGroupingProvider
import io.github.bluegroundltd.outbox.grouping.RandomGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.NullGroupIdProvider
import io.github.bluegroundltd.outbox.grouping.SingleItemGroupingConfiguration
import io.github.bluegroundltd.outbox.grouping.SingleItemGroupingProvider
import io.github.bluegroundltd.outbox.item.OutboxType
Expand Down Expand Up @@ -85,7 +85,7 @@ class TransactionalOutboxBuilderSpec extends UnitTestSpecification {
and:
def groupIdProvider = itemFactory.groupIdProvider
groupIdProvider == builder.groupIdProvider
groupIdProvider instanceof RandomGroupIdProvider
groupIdProvider instanceof NullGroupIdProvider

and:
def groupingProvider = transactionalOutbox.groupingProvider
Expand Down

0 comments on commit 455888b

Please sign in to comment.