How to implement paging query in Akka Persistence

Keywords: Programming SQL Database less github

In Akka Persistence, the data is cached in the service memory (state), and the back-end stores some persistent event logs, so it is impossible to use DSL like SQL for paging query. Using Akka Streams and Actor, we can achieve the effect of paging query by coding, and the paging query is still step-by-step parallel

EventSourcedBehavior

CQRS model is implemented in EventSourcedBehavior of Akka Persistence. Command processing and event processing are decoupled by commandHandler and eventHandler. commandHandler processes the incoming command and returns an event. You can choose to persist the event. If the event needs to be persisted, the event will be passed to eventHandler for processing. After the event is processed, eventHandler will return a "new" state (or directly return to the original state without updating).

def apply[Command, Event, State](
      persistenceId: PersistenceId,
      emptyState: State,
      commandHandler: (State, Command) => Effect[Event, State],
      eventHandler: (State, Event) => State): EventSourcedBehavior[Command, Event, State]

modeling

In terms of the database table modeling we are used to, we will have the following table:

create table t_config
(
    data_id     varchar(64),
    namespace   varchar(64) not null,
    config_type varchar(32) not null,
    content     text        not null,
    constraint t_config_pk primary key (namespace, data_id)
);
create index t_config_idx_data_id on t_config (data_id);

ConfigManager actor can be regarded as t config table. Its entityId is namespace. The State holds the primary key values of all records (ConfigManagerState), which is equivalent to t config IDX data ID index of T config table.

Confidentity actors can be regarded as records stored in the t ﹣ config table, and each actor instance is a row of records. Its entityId consists of namespace + data ID, which is equivalent to the T config PK composite primary key of the T config table. Here we define two EventSourcedBehavior:

  • ConfigManager: has a list of all configuration ID s and saves them as State in EventSourcedBehavior
  • Confidentity: owns each configuration data and saves it as a State in EventSourcedBehavior

Realization

Here is the code of ConfigManager and confidentity. Next, I will explain how to implement paging query.

ConfigManager

object ConfigManager {
  sealed trait Command extends CborSerializable
  sealed trait Event extends CborSerializable
  sealed trait Response extends CborSerializable

  final case class Query(dataId: Option[String], configType: Option[String], page: Int, size: Int) extends Command
  final case class ReplyCommand(in: AnyRef, replyTo: ActorRef[Response]) extends Command
  private final case class InternalResponse(replyTo: ActorRef[Response], response: Response) extends Command

  case class ConfigResponse(status: Int, message: String = "", data: Option[AnyRef] = None) extends Response

  final case class ConfigManagerState(dataIds: Vector[String] = Vector()) extends CborSerializable

  val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigManager")
}

import ConfigManager._
class ConfigManager private (namespace: String, context: ActorContext[Command]) {
  private implicit val system = context.system
  private implicit val timeout: Timeout = 5.seconds
  import context.executionContext
  private val configEntity = ConfigEntity.init(context.system)

  def eventSourcedBehavior(): EventSourcedBehavior[Command, Event, ConfigManagerState] =
    EventSourcedBehavior(
      PersistenceId.of(TypeKey.name, namespace),
      ConfigManagerState(), {
        case (state, ReplyCommand(in, replyTo)) =>
          replyCommandHandler(state, replyTo, in)
        case (_, InternalResponse(replyTo, response)) =>
          Effect.reply(replyTo)(response)
      },
      eventHandler)
  
  private def processPageQuery(
      state: ConfigManagerState,
      replyTo: ActorRef[Response],
      in: Query): Effect[Event, ConfigManagerState] = {
    val offset = if (in.page > 0) (in.page - 1) * in.size else 0
    val responseF = if (offset < state.dataIds.size) {
      Source(state.dataIds)
        .filter(dataId => in.dataId.forall(v => v.contains(dataId)))
        .mapAsync(20) { dataId =>
          configEntity.ask[Option[ConfigState]](replyTo =>
            ShardingEnvelope(dataId, ConfigEntity.Query(in.configType, replyTo)))
        }
        .collect { case Some(value) => value }
        .drop(offset)
        .take(in.size)
        .runWith(Sink.seq)
        .map(items => ConfigResponse(IntStatus.OK, data = Some(items)))
    } else {
      Future.successful(ConfigResponse(IntStatus.NOT_FOUND))
    }
    context.pipeToSelf(responseF) {
      case Success(value) => InternalResponse(replyTo, value)
      case Failure(e)     => InternalResponse(replyTo, ConfigResponse(IntStatus.INTERNAL_ERROR, e.getLocalizedMessage))
    }
    Effect.none
  }
}

ConfigEntity

object ConfigEntity {
  case class ConfigState(namespace: String, dataId: String, configType: String, content: String)

  sealed trait Command extends CborSerializable
  sealed trait Event extends CborSerializable

  final case class Query(configType: Option[String], replyTo: ActorRef[Option[ConfigState]]) extends Command

  final case class ConfigEntityState(config: Option[ConfigState] = None) extends CborSerializable

  val TypeKey: EntityTypeKey[Command] = EntityTypeKey("ConfigEntity")
}

import ConfigEntity._
class ConfigEntity private (namespace: String, dataId: String, context: ActorContext[Command]) {
  def eventSourcedBehavior(): EventSourcedBehavior[Command, Event, ConfigEntityState] =
    EventSourcedBehavior(PersistenceId.of(TypeKey.name, dataId), ConfigEntityState(), commandHandler, eventHandler)

  def commandHandler(state: ConfigEntityState, command: Command): Effect[Event, ConfigEntityState] = command match {
    case Query(configType, replyTo) =>
      state.config match {
        case None =>
          Effect.reply(replyTo)(None)
        case Some(config) =>
          val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None
          Effect.reply(replyTo)(resp)
      }
  }
}

The configmanager ා processpagequery function implements most of the paging query logic (some logic needs to be processed by confidentity).

val offset = if (in.page > 0) (in.page - 1) * in.size else 0
val responseF = if (offset < state.dataIds.size) {
  // process paging
} else {
  Future.successful(ConfigResponse(IntStatus.OK, data = Some(Nil)))
}

Here, first obtain the actual paging data offset offset, and then judge the size of the dataIds saved in the ConfigManager state. If the offset is less than state.dataIds.size, we will perform paging logic, otherwise we will directly return an empty list to the front end.

  Source(state.dataIds)
    .filter(dataId => in.dataId.forall(v => v.contains(dataId)))
    .mapAsync(20) { dataId =>
      configEntity.ask[Option[ConfigState]](replyTo =>
        ShardingEnvelope(s"$namespace@$dataId", ConfigEntity.Query(in.configType, replyTo)))
    }
    .collect { case Some(value) => value }
    .drop(offset)
    .take(in.size)
    .runWith(Sink.seq)
    .map(items => ConfigResponse(IntStatus.OK, data = Some(items)))

This Akka Streams stream is the main implementation of paging processing. If it is SQL, it is similar to:

select * from t_config where data_id like '%"in.dataId"%' offset "offset" limit "in.size"

mapAsync performs 20 concurrent asynchronous operations in the flow execution process, and delegates each matching configeintity (entityId generated by s"$namespace@$dataId") to query the config ﹐ type field. In this way, the complete SQL statement is similar to:

select * from t_config where data_id like '%"in.dataId"%' and change_type = "in.changeType" offset "offset" limit "in.size"

Confidentity's query logic for the change ﹐ type part is as follows:

case Query(configType, replyTo) =>
  state.config match {
    case None =>
      Effect.reply(replyTo)(None)
    case Some(config) =>
      val resp = if (configType.forall(v => config.configType.contains(v))) Some(config) else None
      Effect.reply(replyTo)(resp)
  }

If the in.config type is empty, you don't need to judge the change ﹐ type field, but you can directly return Some(config). The SQL statement is similar at this time:

select * from t_config where data_id like '%"in.dataId"%' and true offset "offset" limit "in.size"

Tip here is a tip. For the judgment of Option[T] field, the. forall method is directly used. It is equivalent to:

option match {
  case Some(x) => p(x)
  case None    => true
}

Summary

The full code can be found here https://github.com/yangbajing/yangbajing-blog/tree/master/src/main/scala/blog/persistence/config Find it.

Posted by kamurj on Wed, 08 Jan 2020 01:01:33 -0800