Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Design actor system #4

Closed
Keith-CY opened this issue Oct 8, 2022 · 13 comments
Closed

Design actor system #4

Keith-CY opened this issue Oct 8, 2022 · 13 comments
Assignees
Labels
documentation Improvements or additions to documentation

Comments

@Keith-CY
Copy link
Member

Keith-CY commented Oct 8, 2022

An actor in actor model is the most basic computing unit to run business logic insulated. It is self-contained that receives messages and processes them sequentially.

Actor runtime is the moderator which decides how, when, where each actor runs, and routes messages transported in the actor system.

Under the control of actor runtime, a large number of actors can perform simultaneously and independently to achieve parallel computation.

Kuai provides the basic implementation of an actor for developers to extend business logic and dominates these actors with the actor runtime for scalability and reliability.

1. Actor Runtime

1.1 Supervision

The main idea delivered by the actor model is Divide-and-Conquer. If a heavy task is assigned to actor_a, the task would be split into sub-tasks and delegated to actor_a's child actors, recursively, until sub-tasks are small enough to be handled in one piece.

Within the runtime, an actor will be activated(created) or deactivated(destroyed) automatically so developers don't have to pay attention to the lifecycle of a single actor. Once a message is sent to an actor located by identity, the actor runtime will activate one to handle the message. And if an actor has been idle for a while(no messages, no internal state), the actor runtime will deactivate it. In some cases, which we will mention later, an actor should be kept alive, it could send messages to itself periodically as a reminder.

To achieve this, a supervision tree is going to be introduced because actor model has a tree-like hierarchical structure. More about supervision tree could be learned from supervision principles

supervision tree

1.2 Router

Actors communicate with each other exclusively by sending messages to targets while targets are not referenced directly, instead, messages are routed by the runtime. With this, actors are decoupled and their lifecycles could be taken over by the runtime.

To deliver a message, the recipient should have an identity that is transparent to the sender. For simplicity, address is used as the identity in Kuai runtime.

There're several ways to get a recipient's address. The simplest way is to register the newly activated actor in a registry. A registry is a local, decentralized, and scalable key-value address storage. It allows an actor to look up one or more actors with a given key. The actors registered in the registry will be managed by the runtime under different supervision/monitoring strategies(strategies could be found in supervision principles too)

The other way is more modular, an actor will only send messages to addresses it received, e.g. a parent spawns a child actor and gets a response of the child actor's address, with the response, the parent could start communicating with its child actor.

2. Actor

Actor

2.1 State

/*
 * field decorated by @State will be registered as an internal state
 */
class CustomActor extends Actor<CustomState> {
  @State()
  state_a: CustomState['state_a']
}

2.2 Behaviour

type Status = ok | error | continue | timeout | stop

class Actor<State, Message> {
  
  /*
   * functions to send messages
   */

  // make a synchronous call
  function call (
    address: ActorAddress,
    message: CustomMessage,
    timeout?: number
  ): {
    status: Status,
    message: CustomMessage,
  }

  // make an asynchronous call
  function cast (
    address: ActorAddress,
    message: CustomMessage,
  ): void

  // send message synchronized to named actors running in specified nodes
  function multi_call (
    nodes: Array<Node>,
    name: string, // registered actor name
    message: CustomMessage,
  ): {
    responses: Array<{
      status: Status,
      message: CustomMessage,
    }>
  }

  // broadcast messages to named actors running in specified nodes
  function abcast (
    nodes: Array<Node>,
    name: string, // registered actor name
    message: CustomMessage,
  ): void

  // reply to the sender in a synchronized call
  function reply (
    client: ActorAddress,
    message: CustomMessage,
  ): void

  /*
   * functions for lifecycle
   */

  // start an actor outside the supervision tree
  function start (
    actorConstructor: ActorConstructor,
    init: CustomState,
    options?: Record<string, string>
  ): {
    status: Status,
    address: ActorAddress,
  }

  // start an actor within the supervision tree
  function startLink (
    actorConstructor: ActorConstructor,
    init: CustomState,
    options?: Record<string, string> 
  ): {
    status: Status,
    address: ActorAddress,
  }

  // stop a actor
  function stop (
    actor: ActorAddress,
    reason: string,
  ): void

}

class CustomActor extends Actor<CustomState, CustomMessage>{

  /*
   * callback behaviors are injected by decorators, and matched by Symbols
   */

  // invoked when the actor is activated
  @Init()
  constructor (
    init: CustomState
  ): { 
    status: Status,
    state: CustomState,
  }

  // invoked when the actor is deactivated
  @Terminate()
  function (
    status: stop,
    reason: string,
    state: CustomState,
  )

  // invoked to handle synchronous call messages, the sender will wait for the response
  @HandleCall(pattern: Symbol)
  function (
    message: CustomMessage,
    from: ActorAddress,
    state: CustomState
  ): {
    status: Status,
    message: CustomMessage,
    state: CustomState,
  }

  // invoked to handle asynchronous call messages, the send won't wait for the response
  @HandleCast(pattern: Symbol)
  function (
    message: CustomMessage,
    state: CustomState,
  ): {
    status: Status,
    state: CustomState,
  }

  // invoked to handle `continue` instruction returned by the previous call
  @HandleContinue()
  function (
    message: CustomMessage,
    state: CustomState,
  }: {
    status: Status,
    state: CustomState,
  }

  // invoked to handle all other unmatched messages
  @HandleInfo()
  function (
    message: any,
    state: CustomState,
  ): {
    status: Status,
    state: CustomState,
  }

  // invoked to inspect internal state of the actor
  @FormatStatus()
  function (
    reason: string,
    state: CustomState,
    context: Context,
  ): void
}

The state field in the parameters is the state of actor before the action and the state field returned by callback is the state of actor after the action(framework will update the internal state of the actor, quite similar to useState hook in React: useAction(preState => curState)).

2.3 Mailbox

// TODO:

3. Basic Models

3.1 Registry

It's actually a Store model we will design later.

3.2 Supervisor

interface ActorSpec {
  name?: Symbol
  id: Symbol
  address: ActorAddress
  strategy: SupervistionStrategy
}

class Supervisor extends Actor {
  @State()
  actors: Map<ActorSpec, Actor>

  constructor (
    children: Array<{
      child: ActorConstructor,
      spec: Pick<ActorSpec, 'strategy'>
    }>,
    options: Record<string, string>,
  ): {
    status: Status
    children: Array<ActorSpec>
  }

  // stop the supervisor
  function stop (
    reason: string,
  )
  
  // add actor children
  function startLink (
    children: Array<{
      child: ActorConstructor, 
      spec: Pick<ActorSpec, 'strategy'>
    }>,
    options: Record<string, string>
  ): {
    status: Status,
    children: Array<ActorSpec>,
  }

  // add a single child
  function startChild (
    actorConstructor: ActorConstructor,
    spec: Pick<ActorSpec, 'strategy'>,
  ): {
    status: Status,
    child: ActorSpec,
  }
    

  // restart a child spec which has been terminated
  function restartChild(
    spec: ActorSpec,
  ): {
    status: Status,
  }

  // delete a child spec which has been terminated
  function deleteChild (
    spec: ActorSpec
  ): {
    status: Status,
  }

  // terminate but keep the child spec
  function terminateChild (
    spec: ActorSpec
  ): {
    status: Status,
  }

}

3.3 Store

// TODO: Design Store model

3.4 Contract

// TODO: Design contract model

3.5 Token

// TODO: Design token model

@Keith-CY Keith-CY added the enhancement New feature or request label Oct 8, 2022
@Keith-CY Keith-CY mentioned this issue Oct 8, 2022
16 tasks
@Keith-CY Keith-CY self-assigned this Oct 14, 2022
@Keith-CY Keith-CY added this to the 2022/10/20 - 2022/10/27 milestone Oct 14, 2022
@Keith-CY Keith-CY added documentation Improvements or additions to documentation and removed enhancement New feature or request labels Oct 25, 2022
@Keith-CY
Copy link
Member Author

The main spec of an actor has been updated, please have a review @homura @felicityin @yanguoyu @IronLu233

@yanguoyu
Copy link
Contributor

@State() is used with property, is it means an actor could have some State?
Is it @State() seem like schema? What is the difference between State and Store?

@Keith-CY
Copy link
Member Author

@State() is used with property, is it means an actor could have some State? Is it @State() seem like schema? What is the difference between State and Store?

An actor has its own internal state, as the illustration shows, an actor consists of a mailbox, its internal state and a set of behaviors.
actor

A property decorated by @State() is a field of the actor's state, and it follows the schema of the actor's state.

What is the difference between State and Store?

A field decorated by State is a field of Store, Store's data is a group of State.

@yanguoyu
Copy link
Contributor

I thought Store is extended from Actor. And users only need to achieve a CustomStore extended from Store.
It seems users need to achieve a CustomActor .

@Keith-CY
Copy link
Member Author

I thought Store is extended from Actor. And users only need to achieve a CustomStore extended from Store. It seems users need to achieve a CustomActor .

Yes, Store derives from Actor, and it will be delivered by the Kuai framework with basic implementation according to a general schema we will design later.

class Store extends Actor<DefaultState, DefaultMessage> {
  @State()
  #state: DefaultState

  @HandleCall(sync)
  function sync (
    message: { blockNumber?: number },
    from: ActorAddress,
    _state: DefaultState,
  ) {
    const state = this.#sync(message.blockNumber)
    from.reply({
      status: ok, 
      message: null,
      state,
    })
  }

  @HandleCall(get)
  function get (
    message: { path: string },
    from: ActorAddress,
    state: DefaultState,
  ) {
    const value = state.get(path)
    from.reply({
      status: ok,
      message: value,
      state,
    })
  }

  @HandleCall(set)
  function set (
    message: { path: string, value: DefaultState['path'] },
    from: ActorAddress,
    state: DefaultState,
  ) {
    state.set(message.path, message.value)
    from.reply({
      status: ok,
      message: null,
      state,
    })
  }

  // ...
}

Developers could implement their own CustomActor for various purposes, like doing some pure computations without storage, but we'll offer some basic models for building a DApp efficiently.

@Keith-CY
Copy link
Member Author

Keith-CY commented Oct 26, 2022

Usually, the Store model but not the Actor model, will be the basic unit of a DApp because Store is quite similar to Actor however it offers some user-friendly get/set APIs.

For example, the Registry mentioned above could be a Store that holds a map of ActorSpec and Actor, and a Config model could also be a Store that holds environment variables.

@homura
Copy link
Contributor

homura commented Oct 26, 2022

I believe this actor model might be cool, but I don't quite understand how this actor model would work with the cell model

The main purpose of Kuai is to help developers lower the barrier, and as we work on the design, I think we can describe how Kuai will be used from the dApp user's and developer user's perspective.

  • a general use case for how a developer will use Kuai to make a dApp, such as register domain name
  • how a dApp user will use a dApp made by Kuai
  • how Kuai's data will be stored on the CKB
  • how to ensure that the on-chain state is trustable

I mention this because I believe Run has a particularly easy to understand design, but Run's NFT metadata is not validated on BSV, and the consensus of these state is ensured by the Run network, which may not be the Kuai wants, because I think Kuai should be a development framework, not a Network. The state of a dApp should be verified by a CKB contract

@Keith-CY
Copy link
Member Author

Keith-CY commented Oct 26, 2022

I believe this actor model might be cool, but I don't quite understand how this actor model would work with the cell model

Actor model is used to structure the dapp better and make data/state sharing less possible, not to map data on-chain to data off-chain.

How to arrange data on-chain and map data on-chain to off-chain will be solved by the Store model.

The main purpose of Kuai is to help developers lower the barrier, and as we work on the design, I think we can describe how Kuai will be used from the dApp user's and developer user's perspective.

  • a general use case for how a developer will use Kuai to make a dApp, such as register domain name
  • how a dApp user will use a dApp made by Kuai
  • how Kuai's data will be stored on the CKB

The example will be delivered by @yanguoyu, that's why I posted this design in draft state, the main spec is necessary for the demo and other parts could be discussed later.

  • how to ensure that the on-chain state is trustable

I don't get the point how to ensure that the on-chain state is trustable

I mention this because I believe Run has a particularly easy to understand design, but Run's NFT metadata is not validated on BSV, and the consensus of these state is ensured by the Run network, which may not be the Kuai wants, because I think Kuai should be a development framework, not a Network. The state of a dApp should be verified by a CKB contract

For now Kuai is not meant to be a network, it includes some conventions, paradigms, a framework runtime, and some basic implementations(model, aggregator service) to pick in the runtime. The state of a DApp should be verified by an on-chain contract, of course.

@felicityin
Copy link
Contributor

Developers need to write contracts to verify the state transition in cell's data. But how can they know which cells are included in the transaction?

@Keith-CY
Copy link
Member Author

Developers need to write contracts to verify the state transition in cell's data. But how can they know which cells are included in the transaction?

IMO, developers don't have to know which cells are going to be packaged in a transaction.

Which cells are going to be packaged is the detail we are going to encapsulate by Kuai.

@felicityin
Copy link
Contributor

developers don't have to know which cells are going to be packaged in a transaction.

OK, but how to write a contract to verify the state transition?

@Keith-CY
Copy link
Member Author

developers don't have to know which cells are going to be packaged in a transaction.

OK, but how to write a contract to verify the state transition?

For now, I don't know how to write a contract to verify the state transition?.

It may be solved by the merkle-like solution, or be handled by a feature we set in the plan

@yanguoyu
Copy link
Contributor

If I will use kuai to recreate a .bit DAPP. The files and directory may be like this.
At first, we want to achieve a function to register a domain.

// file index.ts
import { Actor } from "kuai";
import DomainContract from "./domain/domain.contract";

// actorRoot will be provided by kuai
class RootActor extends Actor {
  constructor() {
    super()
    // kuai will auto collect register contract
    this.add(new DomainContract())
  }
}
const actorRoot = new RootActor()

export function sequencer() {
  // sequencer will be added here
  registerDomain({ name: 'kuai.bit', lock: '0x.1...' })
  registerDomain({ name: 'kuai.bit', lock: '0x.1...' })
  registerDomain({ name: 'kuai1.bit', lock: '0x.1...' })
}

export function registerDomain(params: {
  name: string
  lock: string
}) {
  // Find .bit's actor to handle this action
  actorRoot.send('.bit', {
    method: 'register',
    params
  })
}


export function searchDomain(search: string) {
  // Find .bit's actor to handle this action
  actorRoot.send('.bit', {
    method: 'search',
    params: { search }
  })
}

Then we will achieve DomainContract to handle register messages.

// file domain.contract.ts
// contract is created to handle the contract's business
import { Actor, Handle, PatternHandle } from "kuai";
import { Domain } from "./model/domain";
import DomainStore from "./domain.store";

@PatternHandle('.bit')
class DomainContract extends DomainStore {

  @Handle('register')
  register(domain: Domain, actor: Actor) {
    // do something else before creating the domain
    actor.send('user_contract', { method: '', params: {}})
    this.createDomain(domain)
    // do something else after creating the domain
    actor.send('user_contract', { method: '', params: {}})
  }

  @Handle('search')
  search(search: string) {
    const domains = this.getDomains()
    return domains.filter(v => v.name.includes(search))
  }
}

export default DomainContract

In the end, we will achieve the DomainStore to save data.

// file domain.store.ts
// store is created to handle cell data
import { Store } from "kuai";
import { Domain } from "./model/domain";

export default class DomainStore extends Store<Domain> {
  pattern: string

  getDomains(): Domain[] {
    return this.getData('domain')
  }

  createDomain(domain: Domain) {
    const domains = this.getDomains()
    if (domains.some(v => v.name === domain.name)) {
      throw new Error('domain exists')
    }
    this.saveAsMerkleTree(domain)
  }

  saveAsMerkleTree(domain: Domain) {
    this.setData(`${domain.lock}.domain`, domain)
    // then calculate the Merkle tree and save
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
Archived in project
Development

No branches or pull requests

4 participants