GithubHelp home page GithubHelp logo

Comments (14)

ennru avatar ennru commented on September 27, 2024

See even #26

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

This shouldn't be started until we have finalized the SDK APIs. There are several changes to be completed first. https://github.com/lightbend/akkaserverless-framework/issues/698#issuecomment-858541797

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

This will be switched to the new approach first, without annotations or reflection.

This will also need codegen annotations added to the protocol in framework, including a way to specify the replicated data types (with key/value types where appropriate).

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

including a way to specify the replicated data types (with key/value types where appropriate)

that might become rather verbose? is there a way to require that is specified in the concrete ReplicatedEntity class instead? the downside of that suggestion might be that the first generated class will not compile when those types are unknown

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

including a way to specify the replicated data types (with key/value types where appropriate)

that might become rather verbose? is there a way to require that is specified in the concrete ReplicatedEntity class instead? the downside of that suggestion might be that the first generated class will not compile when those types are unknown

Yeah, the alternative would be generate something without the types. Could be confusing either way — specifying the extra types, or having something that only partially generates.

So I was expecting the code to have the replicated data type: so that it's ReplicatedEntity<ReplicatedCounter> or for shopping cart map just as a test example might be ReplicatedEntity<ReplicatedRegisterMap<String, ShoppingCartDomain.LineItem>>.

So where the codegen annotations have this for Event Sourced Entity:

option (akkaserverless.file).event_sourced_entity = {
  name: "ShoppingCart"
  entity_type: "eventsourced-shopping-cart"
  state: "Cart"
  events: ["ItemAdded", "ItemRemoved"]
};

That could maybe be something like this for a Replicated Entity:

option (akkaserverless.file).replicated_entity = {
  name: "ShoppingCart"
  entity_type: "replicated-shopping-cart"
  replicated_register_map: {
    key: "String"
    value: "LineItem"
  }
};

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

That looks pretty good. I agree that it's good to refer to the proto value (LineItem) like that, since that is similar to how events and state are defined.

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

Not sure what the replicated data types that don't need any extra settings would be: replicated_counter: {} or replicated_counter: "" or something is a bit awkward. Could also do something where there are optional key and value for when these are used, with an enum for the data type. Enums are required by convention to have prefix though. Then it would be:

option (akkaserverless.file).replicated_entity = {
  name: "ShoppingCart"
  entity_type: "replicated-shopping-cart"
  data_type: REPLICATED_DATA_TYPE_COUNTER
};
option (akkaserverless.file).replicated_entity = {
  name: "ShoppingCart"
  entity_type: "replicated-shopping-cart"
  data_type: REPLICATED_DATA_TYPE_REGISTER_MAP
  data_key_type: "String"
  data_value_type: "LineItem"
};

Or for a map that only needs key type:

option (akkaserverless.file).replicated_entity = {
  name: "ShoppingCart"
  entity_type: "replicated-shopping-cart"
  data_type: REPLICATED_DATA_TYPE_COUNTER_MAP
  data_key_type: "MyMessageType"
};

Or for a set with element/value type:

option (akkaserverless.file).replicated_entity = {
  name: "ShoppingCart"
  entity_type: "replicated-shopping-cart"
  data_type: REPLICATED_DATA_TYPE_SET
  data_value_type: "MyMessageType"
};

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

Can we get get better protoc validation from the replicated_register_map: { approach?

I think replicated_counter: {} can work. At least it is consistent for all types and makes it easy for us to evolve/add more things to it if needed.

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

Continuing the discussion on switching away from the mutable API for replicated data in #270 (comment)

We can have specialised replicated entity classes, with their own specific effects.

class MyCounter extends AbstractCounter { // extends ReplicatedCounterEntity
  // optional empty value could be in override method — for something other than the 0 default
  
  @Override public Effect<Response> someMethod(long currentValue, Request request) {
    return effects()
        .increment(request.getValue()) // specific effects for each replicated data type
        .thenReply(Response.newBuilder...);
  }
}

Because the Java Set and Map interfaces have a bunch of mutable methods, that would have to throw unsupported operation exceptions, rather than passing the current values using those interfaces we may want to keep the Replicated* interfaces but with only the read methods. This would be clearer for sets and maps, and it would probably be clearest to do this for all types, so the current value for a counter is not the long value directly, but a ReplicatedCounter with get still.

class MyCounter extends AbstractCounter { // extends ReplicatedCounterEntity
  @Override public Effect<Response> someMethod(ReplicatedCounter counter, Request request) {
    // counter.get() to read current value, no mutating methods
    return effects()
        .increment(request.getValue()) // specific effects for each replicated data type
        .thenReply(Response.newBuilder...);
  }
}

We still have type parameters for keys or values where required:

class MySet extends AbstractSet { // extends ReplicatedSetEntity<MyElement>
  @Override public Effect<Response> someMethod(ReplicatedSet<MyElement> set, Request request) {
    // set.contains(element) and so on, for read-only methods
    return effects()
        .add(request.getSomething()) // specific effects for each replicated data type
        .remove(request.getAnother()) // multiple primary effects is probably simplest
        .thenReply(Response.newBuilder...);
  }
}
class MyCounterMap extends AbstractCounterMap { // extends ReplicatedCounterMapEntity<MyKey>
  @Override public Effect<Response> someMethod(ReplicatedCounterMap<MyKey> counterMap, Request request) {
    // counterMap.contains(key), counterMap.get(key) and so on, for read-only methods
    return effects()
        // counter-specific effect builders
        .update(request.getSomeKey(), effects -> effects.increment(request.getSomeValue()))
        .thenReply(Response.newBuilder...);
  }
}

The ReplicatedMap can store any other replicated data type, and currently has a getOrCreate method accepting a replicated data factory. This doesn't work with the switch to specialised replicated entity classes, and the read-only interfaces. As we now have the specialised replicated maps (ReplicatedCounterMap, ReplicatedRegisterMap, ReplicatedMultiMap), the motivation for using the generic replicated map will be to have a heterogeneous map that combines multiple replicated data objects of different types. We could remove ReplicatedMap altogether, but combining CRDTs still seems useful to support. This could be changed to more of an HMap interface with typed keys:

class MyMap extends AbstractMap { // extends ReplicatedMapEntity<MyKey>

  // the keys can have specialised types, like entities, for have specific effects more easily
  // the key type can still be anything akka serverless serializable, like protobuf message or primitives
  private ReplicatedMap.CounterKey<MyKey> counter1 = ReplicatedMap.createCounterKey(MyKey.for("counter1"));
  private ReplicatedMap.CounterKey<MyKey> counter2 = ReplicatedMap.createCounterKey(MyKey.for("counter2"));
  // the replicated data types that take parameters would accept them on the key
  // note: at this point, it's looking quite similar to creating CRDTs again
  private ReplicatedMap.SetKey<MyKey, MyElement> set1 = ReplicatedMap.createSetKey(MyKey.for("set1"));
  // and what about nesting replicated maps?

  @Override public Effect<Response> someMethod(ReplicatedMap map, Request request) {
    // map.get(counter1) to get the ReplicatedCounter read-only interface for that counter
    // map.get(set1) to get the ReplicatedSet<MyElement> read-only interface
    return effects()
        // specific effects for each replicated data type based on the typed key
        .update(counter1, effects -> effects.increment(request.getSomeValue())
        .update(counter2, effects -> effects.increment(request.getSomeOtherValue())
        .update(set1, effects -> effects.add(request.getSomething()).remove(request.getAnother()))
        .thenReply(Response.newBuilder...);
  }
}

Thinking about ReplicatedMap and the usefulness of combining different CRDTs leads to another idea, which I'll put in its own comment.

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

The automerge project, and the JSON CRDT paper before it, support replicating a JSON-like object with Map and List CRDTs. Looking at the use cases for a heterogeneous ReplicatedMap, a different approach to the above specialised entity types that we could explore is a more automatic conversion of Protobuf messages (or JSON) to a CRDT, similar to automerge.

The default conversion for a Protobuf message could be to a replicated map of registers. While we could replicate the whole object as a register, having each field be its own register would already allow different fields to be updated independently without conflicts.

We would probably need a sequence / list / ordered set CRDT for repeated fields, which we don't have currently.

This would require replicated data specific annotations on the fields for the state Protobuf message. And we'd need to generate the effects API from this, or create a diff between values.

Note that generated effects specific to each field would also start looking like functional lenses, which we may want to start generating for Java for mutating deeper Protobuf structures anyway (ScalaPB already does this too).

This would mean that a replicated entity has a state object, like value entities. I think it already makes sense to see replicated entities as replicated value entities, and that it should start becoming replication across geographic regions or to clients (part of serverless at the edge ideas). Having this approach, where there's a Protobuf message for state with CRDT semantics applied to it, could bring these ideas together more.

So what could this end up looking like: the file-level codegen annotation would be like a value entity:

option (akkaserverless.file).replicated_entity = {
  name: "Something"
  entity_type: "something"
  state: "Thing"
};

While the Thing message needs replication semantics added:

message Thing {
  // defaults to LWW register, if it's not explicitly a counter
  int64 number = 1;
  // to specify a number should have PN counter semantics
  int64 counter = 2 [(akkaserverless.field).replicated_counter];
  // another message becomes a nested map
  AnotherThing another = 3;
}

message AnotherThing {
  // a repeated field could have set semantics, or otherwise default to a sequence CRDT (or to LWW register)
  repeated string tags = 1 [(akkaserverless.field).replicated_set];
  // while a message is already treated as a replicated map, a dynamic map could be created using protobuf map
  // or maybe this is automatic, rather than explicitly annotated
  map<string, int64> dynamic_counters = 2 [(akkaserverless.field).replicated_counter_map];
}

For the generated entity, there could be two approaches. One is to have something that's the same as value entity, and a new state is passed and the current and new states need to be diff'ed to create the replication delta:

class Something extends AbstractSomething { // extends ReplicatedEntity
  @Override public Thing emptyState() {
    return Thing.getDefaultInstance();
  }

  @Override public Effect<Response> someMethod(Thing currentState, Request request) {
    // read from the state as a regular protobuf object
    return effects()
        // We set a new state, and a replication delta is created automatically.
        // For a counter, this compares the current and new states to get an increment delta.
        // While for registers, it just sets to the new value; and so on.
        .updateState(
          currentState.toBuilder()
            .setNumber(request.getValue())
            .setCounter(currentState.getCounter() + request.getIncrement())
            .setAnother(
              currentState.getAnother().toBuilder()
                .setTags(addToList(currentState.getAnother().getTags(), request.getSomething())))
            .build())
        .thenReply(Response.newBuilder...);
  }
}

Or effects are generated for the fields that are typed specifically:

class Something extends AbstractSomething { // extends ReplicatedEntity
  @Override public Thing emptyState() {
    return Thing.getDefaultInstance();
  }

  @Override public Effect<Response> someMethod(Thing currentState, Request request) {
    // read from the state as a regular protobuf object
    return effects()
        .update(
          // effect builder generated specifically for fields in Thing, looks similar to lenses
          thing -> thing.number.set(request.getValue()),
          thing -> thing.counter.increment(request.getIncrement()),
          thing -> thing.another.tags.add(request.getSomething()))
        .thenReply(Response.newBuilder...);
  }
}

For something like just a replicated counter, it would still be wrapped in a message, just like a value entity counter. The conversion could optimise this and avoid the replicated map wrapper though.

Anyway, not sure yet if there are any barriers to doing this, or whether we don't want to tie this so much to Protobuf, but I think it's worthwhile considering this API for replicated entities, which is much more of a replicated value entity.

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

I think my preference would be to not have generated effects, something like this:

class MySet extends AbstractSet { // extends ReplicatedSetEntity<MyElement>
  @Override public Effect<Response> someMethod(ReplicatedSet<MyElement> set, Request request) {
    // set.contains(element) and so on, for read-only methods
    ReplicatedSet<MyElement> updatedSet =
      set
        .add(request.getSomething())
        .remove(request.getAnother());
    return effects()
        .update(updatedSet)
        .thenReply(Response.newBuilder...);
  }
}

Looking at that I wonder how important it is to have them immutable? The strange thing in my opinion would be to not have the explicit effects().update(updatedSet). I'm fine with changing them to immutable, but it might not be critical for the Java dev experience.

The second idea of Thing currentState is interesting, but too big change right now, since we want to wrap up this week. It is definitely something we should create a separate issue for and evaluate more.

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

Ok, so that's the immutable + update effect approach that was discussed earlier.

my preference would be to not have generated effects

Effects wouldn't be generated, but specific to each replicated data type (each specialised replicated entity class). So a ReplicatedCounterEntity would have increment and decrement effects, while a ReplicatedSetEntity would have add and remove effects, and so on.

Looking at that I wonder how important it is to have them immutable? The strange thing in my opinion would be to not have the explicit effects().update(updatedSet). I'm fine with changing them to immutable, but it might not be critical for the Java dev experience.

The only thing with keeping it mutable will be what happens if it's not passed to update? The underlying structure has been changed. Do we end up with a "clone" first approach, to make sure that mutations without update effect are discarded?

Also, creating an updatedSet would need to be a new immutable API. That's not possible now as the current add / remove (implementing java.util.Set) return boolean, given that it's the mutable API. So leaving the current mutable APIs + requiring an update effect would need to be:

class MySet extends AbstractSet { // extends ReplicatedSetEntity<MyElement>
  @Override public Effect<Response> someMethod(ReplicatedSet<MyElement> set, Request request) {
      // copy of replicated set is actually passed, to ensure it's passed to update or otherwise ignored
      set.add(request.getSomething());
      set.remove(request.getAnother());
    return effects()
        .update(set)
        .thenReply(Response.newBuilder...);
  }
}

from kalix-jvm-sdk.

patriknw avatar patriknw commented on September 27, 2024

Effects wouldn't be generated, but specific to each replicated data type (each specialised replicated entity class). So a ReplicatedCounterEntity would have increment and decrement effects, while a ReplicatedSetEntity would have add and remove effects, and so on.

Got it!

I still like the consistency of only one primary effect updateState in the same way as with value entity.

Do we end up with a "clone" first approach, to make sure that mutations without update effect are discarded?

I think that is fine. If it's a performance problem we could make a shallow clone and only deep clone on the first mutating operation.

from kalix-jvm-sdk.

pvlugter avatar pvlugter commented on September 27, 2024

Follow up issues: #298 (immutable data structures) and #299 ("auto replication" idea).

from kalix-jvm-sdk.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.