How to query the state store in the Kafka Streams DSL to implement consumer idempotency - apache-kafka

I'm working in an scenario where duplicated messages could arrive at a consumer (a KStream application). To use the typical case let's suppose it's an OrderCreatedEvent and the KStream has a logic that processes the order. The event has an order-id that would help me identify duplicated messages.
What I want to do is:
1) Add every order to a persistent state store
2) When processing the message in the KStream, query the state store to check if the message had already been received, not doing anything in that case.
val persistentKeyValueStore = Stores.persistentKeyValueStore("order-store")
val stateStore: Materialized<Int, Order, KeyValueStore<Bytes, ByteArray>> =
Materialized.`as`<Int, Order>(persistentKeyValueStore)
.withKeySerde(intSerde)
.withValueSerde(orderSerde)
val orderTable: KTable<Int, Order> = input.groupByKey(Serialized.with(intSerde, orderSerde))
.reduce({ _, y -> y }, stateStore)
var orderStream: KStream<Int, Order> = ...
orderStream.filter { XXX }
.map { key, value ->
processingLogic()
KeyValue(key, value)
}...
In the filter { XXX } bit I would like to query the state store check if the order id is there (let's assume the order is used as the key of the keyvaluestore), filtering out orders already processed (present in the state store).
My first question is: how can I query a state store in the KStream DSL, e.g. inside the filter operation.
Second question: in this case, how can I handle the arrival of a new (not previously processed message)? If the KTable persists the order to the state store BEFORE the orderStream KStream execution the message would already be in the store. They should be added only after the processing has completed.
How can I do this? It's likely I shouldn't be using a KTable for it but something like:
orderStream.filter { keystore.get(key) == null }
.map { key, value ->
processingLogic()
KeyValue(key, value)
}
.foreach { key, value ->
keystore.put(key, value);
}

Following Matthias' indications I implemented it like this:
DeduplicationTransformer
package com.codependent.outboxpattern.operations.stream
import com.codependent.outboxpattern.account.TransferEmitted
import org.apache.kafka.streams.KeyValue
import org.apache.kafka.streams.kstream.Transformer
import org.apache.kafka.streams.processor.ProcessorContext
import org.apache.kafka.streams.state.KeyValueStore
import org.slf4j.LoggerFactory
#Suppress("UNCHECKED_CAST")
class DeduplicationTransformer : Transformer<String, TransferEmitted, KeyValue<String, TransferEmitted>> {
private val logger = LoggerFactory.getLogger(javaClass)
private lateinit var dedupStore: KeyValueStore<String, String>
private lateinit var context: ProcessorContext
override fun init(context: ProcessorContext) {
this.context = context
dedupStore = context.getStateStore(DEDUP_STORE) as KeyValueStore<String, String>
}
override fun transform(key: String, value: TransferEmitted): KeyValue<String, TransferEmitted>? {
return if (isDuplicate(key)) {
logger.warn("****** Detected duplicated transfer {}", key)
null
} else {
logger.warn("****** Registering transfer {}", key)
dedupStore.put(key, key)
KeyValue(key, value)
}
}
private fun isDuplicate(key: String) = dedupStore[key] != null
override fun close() {
}
}
FraudKafkaStreamsConfiguration
const val DEDUP_STORE = "dedup-store"
#Suppress("UNCHECKED_CAST")
#EnableBinding(TransferKafkaStreamsProcessor::class)
class FraudKafkaStreamsConfiguration(private val fraudDetectionService: FraudDetectionService) {
private val logger = LoggerFactory.getLogger(javaClass)
#KafkaStreamsStateStore(name = DEDUP_STORE, type = KafkaStreamsStateStoreProperties.StoreType.KEYVALUE)
#StreamListener
#SendTo(value = ["outputKo", "outputOk"])
fun process(#Input("input") input: KStream<String, TransferEmitted>): Array<KStream<String, *>>? {
val fork: Array<KStream<String, *>> = input
.transform(TransformerSupplier { DeduplicationTransformer() }, DEDUP_STORE)
.branch(Predicate { _: String, value -> fraudDetectionService.isFraudulent(value) },
Predicate { _: String, value -> !fraudDetectionService.isFraudulent(value) }) as Array<KStream<String, *>>
...

Related

ReactiveStreamCrudRepository not returning data from postgres DB

I'm new to reactive programming and micronaut. I'm basically working on simple CRUD APIs. I'm using Kotlin with micronaut. I'm not sure why the DB is not returning any Data and I'm stuck with this.
#JdbcRepository(dialect = Dialect.POSTGRES)
interface EmployeeCrudRepository: ReactiveStreamsCrudRepository<EmployeeMaster, Int>, EmployeeRepository {
}
interface EmployeeRepository {
fun findByEmployeeIdAndTcin(employeeId: UUID, tcin: String): Mono<EmployeeMaster>
}
#MappedEntity
#Table(name="employee")
data class EmployeeMaster (
#Id
#Column(name = "transaction_id")
val transactionId: Int,
#Column(name = "employee_id")
val employeeId: UUID,
#Column(name = "item_id")
val itemId: UUID
)
fun getEmployeeDetailsResponse(registryId: UUID, itemId: String) : Mono<EmployeeDetailsDTO> {
return getEmployeeDetails(employeeId, itemId)
.map {
employeeDetails -> EmployeeDetailsDTO(employeeDetails)
}
.switchIfEmpty {
logger.info("No records found")
Mono.just(ItemDetailsDTO())
}
}
fun getEmployeeDetails(employeeId: UUID, itemId: String) : Mono<EmployeeDetailsDTO> {
return employeeRepository.findByEmployeeIdAndTcin(registryId = registryId, tcin = itemId)
.map {
employeeDetails -> EmployeeDetailsDTO(employeeDetails)
}
.switchIfEmpty {
logger.info("No records found")
Mono.just(EmployeeDetailsDTO())
}
}
I'm confused as to how to debug this to find the issue. The credentials all seem to be fine and the record I'm searching for exists in the DB.
flyway {
// ./gradlew -Ppostgres_host=localhost -Ppostgres_ssl='' -Ppostgres_user=postgres -Ppostgres_pwd=postgres flywayMigrate -i
url = "jdbc:postgresql://${postgres_host}:5432/postgres${postgres_ssl}"
user = "${postgres_user}"
password = "${postgres_pwd}"
schemas = ['public']
}
Issue Found:
My Bad, I was sending some other value and didn't realise that the value was incorrect. The implementation was fine and returning the response as expected. I'm writing kotlin and micronaut code for the first time and at the back of my head it always feels like the implementation was wrong.
Take a look at Access a Database with Micronaut Data R2DBC.
There are a number of things in your example that look wrong:
#JdbcRepository(dialect = Dialect.POSTGRES) should be #R2dbcRepository.
#Table and #Column are JPA.
#MappedEntity("employee")
#MappedProperty("item_id")
You might not need #MappedProperty("item_id"), itemId should be mapped to item_id.

How I can update my adapter for RecyclerView after change my LiveData?

I created a fragment that in the onActivityCreated method fetches Firebase data by limiting the query to a calendar date. Then I place Observers on my LiveData that are inside my ViewModel and that will deliver the list to my Adapter.
If I add, remove or update items in the same list, the changes are sent to firebase and the adapter reflects them on the screen. It works ok.
But, I am trying to develop a filter button, which will basically change the deadline date for the Firebase query. When I select a particular filter, the viewModel needs to retrieve the data from Firebase limited to the filter date. This generates a new list, having a different size from the previous one.
However, when the query occurs, the Adapter's getItemCount() method stores the size of the last list. This fact confuses the Adapter and the functions notifyItemInserted and notifyItemRemoved end up making confusing animations on the screen after changing the filter. I dont know whats is wrong.
How can I correctly observes LiveData and tell the adapter? Am I making a mistake in the MVVM architecture or forgetting some function?
My Fragment:
class HistoryFragment : Fragment(), OnItemMenuRecyclerViewClickListener {
private lateinit var mSecurityPreferences: SecurityPreferences
private lateinit var viewModel: BalancesViewModel
private lateinit var adapter: BalancesAdapter
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
setHasOptionsMenu(true)
viewModel = ViewModelProvider(this).get(BalancesViewModel::class.java)
adapter = BalancesAdapter(requireContext())
mSecurityPreferences = SecurityPreferences(requireContext())
return inflater.inflate(R.layout.fragment_history, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setupFilter()
//Setup adapter
adapter.listenerMenu = this
recycler_view_history.adapter = adapter
//Fetch data based in filter by date
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
// Put logic to listen RealTimeUpdates
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.balances.observe(viewLifecycleOwner, Observer {
adapter.setBalances(it)
})
viewModel.balance.observe(viewLifecycleOwner, Observer {
adapter.addBalance(it)
})
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.history_menu_filter, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.item_menu_filter_this_month -> {
updateFilter(THIS_MONTH)
}
R.id.item_menu_filter_two_months -> {
updateFilter(TWO_MONTHS)
}
R.id.item_menu_filter_last_six_months -> {
updateFilter(LAST_SIX_MONTHS)
}
R.id.item_menu_filter_all -> {
updateFilter(ALL_MONTHS)
}
}
return super.onOptionsItemSelected(item)
}
private fun setupFilter() {
var filterOption = mSecurityPreferences.getStoredLong(FILTER_DATE)
if (filterOption == 0L){
filterOption = HandleDate.getLongToFilter(LAST_SIX_MONTHS)
mSecurityPreferences.storeLong(FILTER_DATE, filterOption)
}
}
private fun updateFilter(filterOption: Int){
val newFilterOption = HandleDate.getLongToFilter(filterOption)
mSecurityPreferences.storeLong(FILTER_DATE, newFilterOption)
updateUI()
}
private fun updateUI(){
viewModel.fetchBalances(mSecurityPreferences.getStoredLong(FILTER_DATE))
viewModel.getRealTimeUpdates(mSecurityPreferences.getStoredLong(FILTER_DATE))
}
}
My ViewModel:
class BalancesViewModel : ViewModel() {
private val userReference = FirebaseAuth.getInstance().currentUser!!.uid
private val dbUserReference = FirebaseDatabase.getInstance().getReference(userReference)
private val _balances = MutableLiveData<List<Balance>>()
val balances: LiveData<List<Balance>>
get() = _balances
private val _balance = MutableLiveData<Balance>()
val balance: LiveData<Balance>
get() = _balance
private val _result = MutableLiveData<Exception?>()
val result: LiveData<Exception?>
get() = _result
fun addBalance(balance: Balance) {
balance.id = dbUserReference.push().key
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
private val childEventListener = object : ChildEventListener {
override fun onCancelled(error: DatabaseError) {
}
override fun onChildMoved(snapshot: DataSnapshot, p1: String?) {
}
override fun onChildChanged(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildAdded(snapshot: DataSnapshot, p1: String?) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
_balance.value = balance
}
override fun onChildRemoved(snapshot: DataSnapshot) {
val balance = snapshot.getValue(Balance::class.java)
balance?.id = snapshot.key
balance?.isDeleted = true
_balance.value = balance
}
}
fun getRealTimeUpdates(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble()).addChildEventListener(childEventListener)
}
fun fetchBalances(longLimitDate: Long) {
dbUserReference.child(NODE_BALANCES).orderByChild(COLUMN_DATE_MILLI)
.startAt(longLimitDate.toDouble())
.addListenerForSingleValueEvent(object : ValueEventListener {
override fun onCancelled(error: DatabaseError) {}
override fun onDataChange(snapshot: DataSnapshot) {
if (snapshot.exists()) {
val listBalances = mutableListOf<Balance>()
for (balanceSnapshot in (snapshot.children)) {
val balance = balanceSnapshot.getValue(Balance::class.java)
balance?.id = balanceSnapshot.key
balance?.let { listBalances.add(it) }
}
listBalances.sortByDescending { it.dateMilli }
_balances.value = listBalances
}
}
})
}
fun updateBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(balance)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
fun deleteBalance(balance: Balance) {
dbUserReference.child(NODE_BALANCES).child(balance.id!!).setValue(null)
.addOnCompleteListener {
if (it.isSuccessful) {
_result.value = null
} else {
_result.value = it.exception
}
}
}
My Adapter:
class BalancesAdapter(private val context: Context) :
RecyclerView.Adapter<BalancesAdapter.BalanceViewModel>() {
private var balances = mutableListOf<Balance>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
BalanceViewModel(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_recyclerview_balance, parent, false)
)
override fun getItemCount() = balances.size
override fun onBindViewHolder(holder: BalanceViewModel, position: Int) {
holder.view.text_view_value_balance_item.text = balances[position].value
holder.view.text_view_date_item.text = balances[position].date
}
fun setBalances(balances: List<Balance>) {
this.balances = balances as MutableList<Balance>
notifyDataSetChanged()
}
fun addBalance(balance: Balance) {
val index = balances.indexOf(balance)
if (!balances.contains(balance)) {
balances.add(balance)
notifyItemInserted(index)
} else {
if (balance.isDeleted) {
balances.removeAt(index)
notifyItemRemoved(index)
} else {
balances[index] = balance
}
}
notifyItemRangeChanged(index, itemCount)
}
class BalanceViewModel(val view: View) : RecyclerView.ViewHolder(view)
}
Tnks for your attention.
Okay, it's been 4 days since I asked this question and after feeling a little frustrated with the project I come back here on StackOverFlow to post my own answer.
The problematic issue within the code I showed is in my Adapter's addBalance method.
When I created the Balance data model, I set the isDeleted attribute to identify that it was deleted. Upon entering Firebase it receives a NULL value and therefore it ceases to exist.
Then, as I have two listeners (one defined in the addListenerForSingleValueEvent method and the other defined in the addChildEventListener method), one ends up triggering the other when there is a change in the Firebase data, but I don't want to go into detail on that issue. The fact is that I checked that my addBalance method was being called after I deleted an object, causing that object to be inserted back into the Adapter's data list, even before the removal operation ended in Firebase.
So I changed the logic of my method to make sure that my object was deleted and only included it in my Adapter list after checking the isDeleted attribute.
fun dealWithBalance(balance: Balance){
val index = balances.indexOf(balance)
if(balance.isDeleted && balances.contains(balance)){
balances.removeAt(index)
notifyItemRemoved(index)
} else if(!balance.isDeleted && !balances.contains(balance)){
balances.add(balance)
} else if(index >= 0){
balances[index] = balance
notifyItemChanged(index)
}
}
I renamed addBalance to dealWithBalance...

Save in ReactiveCrudRepository not inserting or updating records

As stated in the title I'm not been able to insert or update records in my Postgres database.
I just started working with spring and kotlin so there might be some pretty basic configuration that it's missing. Thanks in advance
Here is my code base
UserRepository
#Repository
interface UserRepository : ReactiveCrudRepository<User, Int> {}
User model
#Table("user_app")
data class User (
#Id
#Column("id")
#GeneratedValue(strategy=GenerationType.IDENTITY)
val id : Int? = null,
#Column("username")
val username : String?,
#Column("email")
val email : String?,
#Column("name")
val name : String?,
#Column("password")
val password : String?
)
UserController
#Component
class UserController{
#Autowired
private lateinit var userRepository : UserRepository
fun getAllUsers() : Flux<User> = userRepository.findAll()
fun getUserById( userId: Int) : Mono<User> = userRepository.findById(userId)
fun createUser(user: User): Mono<User> = userRepository.save(user)
fun updateUser(user: User) : Mono<User> = userRepository.save(user)
}
UserConfiguration
#Configuration
class UserConfiguration {
#FlowPreview
#Bean
fun userRouter(controller: UserController): RouterFunction<ServerResponse> = router{
("/users").nest{
GET("") { _ ->
ServerResponse.ok().body(controller.getAllUsers())
}
GET("/{id}") { req ->
ServerResponse.ok().body(controller.getUserById(req.pathVariable("id").toInt()))
}
accept(MediaType.APPLICATION_JSON)
POST("") {req ->
ServerResponse.ok().body(req.bodyToMono(User::class.java).doOnNext { user ->
controller.createUser(user) })
}
accept(MediaType.APPLICATION_JSON)
PUT("") {req ->
ServerResponse.ok().body(req.bodyToMono(User::class.java).doOnNext { user ->
run {
controller.updateUser(user)
}
})
}
}
}
}
R2dbcConfiguration
#Configuration
#EnableR2dbcRepositories
class R2dbcConfiguration : AbstractR2dbcConfiguration() {
#Bean
override fun connectionFactory(): PostgresqlConnectionFactory {
val config = PostgresqlConnectionConfiguration.builder()
.host("localhost")
.port(5432)
.database("postgres")
.username("postgres")
.password("pass123")
.build()
return PostgresqlConnectionFactory(config)
}
}
Found out why this wasnt working.
Save does not persist the changes in the database, to do that you need to call some methods. I called subscribe but because this is a blocking method I ran it either in a let block or a doOnNext
fun createUser(request: ServerRequest): Mono<ServerResponse> {
println("createUser")
val badRequest = ServerResponse.badRequest().build()
val user = request.bodyToMono(User::class.java)
return user
.doOnNext { u -> userRepository.save(u).subscribe() }
.flatMap {
ServerResponse.ok().contentType(APPLICATION_JSON).body(fromValue(it))
}
}

Transform Mono<Void> in Mono<String> adding a value

My program services offer some delete methods that return Mono<Void>, e.g.: fun delete(clientId: String) : Mono<Void>
After calling .delete("x") I would like to propagate the clientId downstream to do other operations:
userService.get(id).map{ user ->
userService.delete(user.id) //This returns Mono<Void>
.map {
user.id //Never called!!!
}
.map { userId ->
//other calls using the propagated userId
}
}
The problem is since delete returns a Mono<Void>, the following .map {
user.id } is never called. So how can I transform the Mono<Void> into a Mono<String> to propagate the userId?
You can use thenReturn operator:
userService.get(id)
.flatMap { user -> userService.delete(user.id).thenReturn(user.id) }
.flatMap { id -> //other calls using the propagated userId }
I managed to work around it using hasNext that transforms it into a Boolean:
#Test
fun `should`() {
val mono: Mono<String> = "1".toMono()
.flatMap { id ->
val map = Mono.empty<Void>()
.hasElement()
.map {
id + "a"
}.map {
(it + "1")
}
map
}
mono.doOnNext {
println(mono)
}.subscribe()
}

Implement your own object binder for Route parameter of some object type in Play scala

Well, I want to replace my String param from the following Play scala Route into my own object, say "MyObject"
From GET /api/:id controllers.MyController.get(id: String)
To GET /api/:id controllers.MyController.get(id: MyOwnObject)
Any idea on how to do this would be appreciated.
Well, I have written up my own "MyOwnObject" binder now. Another way of implementing PathBindable to bind an object.
object Binders {
implicit def pathBinder(implicit intBinder: PathBindable[String]) = new PathBindable[MyOwnObject] {
override def bind(key: String, value: String): Either[String, MyOwnObject] = {
for {
id <- intBinder.bind(key, value).right
} yield UniqueId(id)
}
override def unbind(key: String, id: UniqueId): String = {
intBinder.unbind(key, id.value)
}
}
}
Use PathBindable to bind parameters from path rather than from query. Sample implementation for binding ids from path separated by comma (no error handling):
public class CommaSeparatedIds implements PathBindable<CommaSeparatedIds> {
private List<Long> id;
#Override
public IdBinder bind(String key, String txt) {
if ("id".equals(key)) {
String[] split = txt.split(",");
id = new ArrayList<>(split.length + 1);
for (String s : split) {
long parseLong = Long.parseLong(s);
id.add(Long.valueOf(parseLong));
}
return this;
}
return null;
}
...
}
Sample path:
/data/entity/1,2,3,4
Sample routes entry:
GET /data/entity/:id controllers.EntityController.process(id: CommaSeparatedIds)
I'm not sure if it works for binding data in the path part of a URL, but you may want to read the docs on QueryStringBindable if you're able to accept your data as query params.