Krzysztof Borowski
Diagram showing the parts of a mature flower. In this example the perianth is separated into a calyx (sepals) and corolla (petals)
brew install kalix
kalix auth login
kalix projects new <projectname> "project description" --region=<region>
kalix config set project <projectname>
kalix docker add-credentials --docker-server <my-server> \
--docker-username <my-username> \
--docker-email <my-email> \
--docker-password <my-password>
#!/bin/sh
sbt Docker/publish -Ddocker.username=liosedhel &&
docker tag liosedhel/phototrip:$1 &&
docker push liosedhel/phototrip:$1 &&
kalix service deploy phototrip liosedhel/phototrip:$1
kalix services expose my-service
kalix projects list
kalix logs --raw phototrip
kalix svc list
kalix svc components list phototrip
message CreateWorldMap {
string map_id = 1 [(kalix.field).entity_key = true];
string creator_id = 2;
string description = 3;
}
service WorldMapService {
option (kalix.codegen) = {
value_entity: {
name: "com.virtuslab.phototrip.worldmap.domain.WorldMapValueEntity"
entity_type: "worldmap"
state: "com.virtuslab.phototrip.worldmap.domain.WorldMapState"
}
};
option (kalix.service).acl.allow = { principal: ALL };
rpc Create (CreateWorldMap) returns (google.protobuf.Empty);
rpc Get (GetWorldMap) returns (CurrentWorldMap) {
option (google.api.http) = {
get: "/worldmaps/{worldmap_id}"
};
};
}
class WorldMapValueEntity(context: ValueEntityContext)
extends AbstractWorldMapValueEntity {
override def emptyState: WorldMap = WorldMap()
override def create(
currentState: WorldMap,
createWorldMap: CreateWorldMap
): ValueEntity.Effect[Empty] = {
if(!isValid(createWorldMap)) {
effects.error(s"Invalid $createWorldMap command")
} else {
effects
.updateState(toWorldMap(createWorldMap))
.thenReply(Empty.defaultInstance)
}
}
override def get(
currentState: WorldMap,
getWorldMap: GetWorldMap
): ValueEntity.Effect[CurrentWorldMap] = {
if (currentState == emptyState) {
effects.error(s"Map ${getWorldMap.worldmapId} does not exist")
} else {
effects.reply(toCurrentWorldMap(currentState))
}
}
...
}
service PlaceService {
option (kalix.codegen) = {
event_sourced_entity: {
name: "com.virtuslab.phototrip.place.domain.PlaceEventSourcedEntity"
entity_type: "place"
state: "com.virtuslab.phototrip.place.domain.Place"
events: [
"com.virtuslab.phototrip.place.domain.PlaceCreated",
"com.virtuslab.phototrip.place.domain.PhotoLinkAdded"
]
}
};
option (kalix.service).acl.allow = { principal: ALL };
rpc CreatePlace (CreateNewPlace) returns (google.protobuf.Empty);
rpc AddPhotoLink (AddPhotoLinkUrl) returns (google.protobuf.Empty);
rpc Get (GetPlace) returns (CurrentPlace) {
option (google.api.http) = {
get: "/places/{place_id}"
};
};
}
class PlaceEventSourcedEntity(context: EventSourcedEntityContext)
extends AbstractPlaceEventSourcedEntity {
override def emptyState: Place = Place()
override def createPlace(
currentState: Place,
createNewPlace: CreateNewPlace
): EventSourcedEntity.Effect[Empty] =
effects.emitEvent(
PlaceCreated(
createNewPlace.placeId,
createNewPlace.mapId,
createNewPlace.description,
createNewPlace.coordinates
)
).thenReply(_ => Empty.defaultInstance)
override def placeCreated(
currentState: Place,
placeCreated: PlaceCreated
): Place =
Place(
placeCreated.placeId,
placeCreated.mapId,
placeCreated.description,
placeCreated.coordinates,
Nil
)
service UserService {
option (kalix.codegen) = {
replicated_entity: {
name: "com.virtuslab.phototrip.user.domain.UserReplicatedEntity"
entity_type: "user"
replicated_register: {
value: "com.virtuslab.phototrip.user.domain.User"
}
}
};
option (kalix.service).acl.allow = { principal: ALL };
rpc Create (CreateNewUser) returns (google.protobuf.Empty) {
option (google.api.http) = {
post: "/users"
};
};
rpc Get (GetUser) returns (com.virtuslab.phototrip.user.domain.User) {
option (google.api.http) = {
get: "/users/{user_id}"
};
};
}
class UserReplicatedEntity(context: ReplicatedEntityContext)
extends AbstractUserReplicatedEntity {
override def emptyValue: User = User()
override def create(
currentData: ReplicatedRegister[User],
createNewUser: CreateNewUser
): ReplicatedEntity.Effect[Empty] = {
effects
.update(
currentData.set(
User(createNewUser.userId, createNewUser.email, createNewUser.nick)
)
).thenReply(Empty.defaultInstance)
}
def get(
currentData: ReplicatedRegister[User],
getUser: api.GetUser
): ReplicatedEntity.Effect[User] = {
currentData.get match {
case Some(user) => effects.reply(user)
case None =>
effects.error("User does not exist", io.grpc.Status.Code.NOT_FOUND)
}
}
}
service WorldMapByUserId {
option (kalix.codegen) = {
view: {}
};
rpc UpdateWorldMap(domain.WorldMapState) returns (WorldMapView) {
option (kalix.method).eventing.in = {
value_entity: "worldmap"
};
option (kalix.method).view.update = {
table: "worldmaps"
transform_updates: true
};
}
rpc GetWorldMaps(ByUserIdRequest) returns (stream WorldMapView) {
option (kalix.method).view.query = {
query: "SELECT * FROM worldmaps WHERE creator_id = :user_id"
};
option (google.api.http) = {
get: "/worldmap/user/{user_id}"
};
}
}
class WorldMapByUserIdView(context: ViewContext)
extends AbstractWorldMapByUserIdView {
override def emptyState: WorldMapView = WorldMapView.defaultInstance
override def updateWorldMap(
state: WorldMapView,
worldMapState: WorldMapState
): UpdateEffect[WorldMapView] = {
effects.updateState(
WorldMapView(
worldMapState.mapId,
worldMapState.creatorId,
worldMapState.description
)
)
}
}
aka glue it together
$ kalix projects config set broker \
--broker-service kafka \
--broker-config-file <kafka-broker-config-file>
ccloud kafka topic create TOPIC_ID
service WorldMapAndPlacesEventsToTopic {
option (kalix.codegen) = {
action: {}
};
rpc PlaceCreation (com.virtuslab.phototrip.place.domain.PlaceCreated)
returns (PlaceCreatedMessage) {
option (kalix.method).eventing.in = {
event_sourced_entity: "place"
ignore_unknown: true
};
option (kalix.method).eventing.out = {
topic: "analytics-events"
};
}
rpc WorldMapUpdate (com.virtuslab.phototrip.worldmap.domain.WorldMapState)
returns (WorldMapUpdatedMessage) {
option (kalix.method).eventing.in = {
value_entity: "worldmap"
};
option (kalix.method).eventing.out = {
topic: "analytics-events"
};
}
}
class WorldMapAndPlacesEventsToTopicAction(creationContext: ActionCreationContext)
extends AbstractWorldMapAndPlacesEventsToTopicAction {
override def placeCreation(
placeCreated: PlaceCreated
): Action.Effect[PlaceCreatedMessage] = {
effects.reply(
PlaceCreatedMessage(
placeCreated.placeId,
placeCreated.mapId,
placeCreated.description,
placeCreated.coordinates.map(c => Coordinates(c.latitude, c.latitude))
)
)
}
override def worldMapUpdate(
worldMapState: WorldMapState
): Action.Effect[WorldMapUpdatedMessage] = {
effects.reply(
WorldMapUpdatedMessage(
worldMapState.mapId,
worldMapState.creatorId,
worldMapState.description
)
)
}
}
service ReadFromAnalyticsTopic {
option (kalix.codegen) = {
action: {}
};
rpc PlaceCreation (com.virtuslab.phototrip.worldmap.actions.PlaceCreatedMessage)
returns (google.protobuf.Empty) {
option (kalix.method).eventing.in = {
topic: "analytics-events"
};
}
rpc WorldMapUpdate (com.virtuslab.phototrip.worldmap.actions.WorldMapUpdatedMessage)
returns (google.protobuf.Empty) {
option (kalix.method).eventing.in = {
topic: "analytics-events"
};
}
}
class ReadFromAnalyticsTopicAction(creationContext: ActionCreationContext)
extends AbstractReadFromAnalyticsTopicAction {
private val log = LoggerFactory.getLogger(classOf[ReadFromAnalyticsTopicAction])
override def placeCreation(
placeCreatedMessage: PlaceCreatedMessage
): Action.Effect[Empty] = {
log.info(s"PubSub:Read $placeCreatedMessage")
effects.forward(
components.statsValueEntity.placeCreation(
NewPlace(StatsValueEntity.key, placeCreatedMessage.placeId)
)
)
}
override def worldMapUpdate(
worldMapUpdatedMessage: WorldMapUpdatedMessage
): Action.Effect[Empty] = {
log.info(s"PubSub:Read $worldMapUpdatedMessage")
effects.forward(
components.statsValueEntity.mapUpdate(
NewMap(StatsValueEntity.key, worldMapUpdatedMessage.mapId)
)
)
}
}
kalix svc c list-entity-ids phototrip \
com.virtuslab.phototrip.place.api.PlaceService
kalix svc c get-state phototrip \
com.virtuslab.phototrip.place.api.PlaceService Place1 --output json
kalix svc c list-events phototrip \
com.virtuslab.phototrip.place.api.PlaceService Place1 --output json
kalix svc views list phototrip
Pros
Cons
Pros
Cons
Pros
Cons
Pros
Cons
docker-compose up -d
sbt run
Pros
Cons
kalix svc start phototrip
kalix svc expose phototrip
kalix svc components list phototrip
kalix svc views list phototrip
kalix svc delete phototrip
Pros
Cons
kalix service deploy \
phototrip liosedhel/phototrip:55
Pros
Cons
Pros
Cons
Pros
Cons
You can get a custom deal, see https://discuss.kalix.io/t/minimum-per-service-cost-on-pay-as-you-go/163.
Additionally, the feature to put some upper bound or tweak autoscaling service capability is on the way - this should reduce unexpected costs in case of unpredicted service traffic.
IAM integration - no way to integrate directly with existing organisation accounts.
No direct integration network (e.g. VPC Network Peering) between Kalix platform and client infrastructure (available with custom deal).
Limited views capabilities (features in progress).
Scalability only within a particular region (feature in progress).
No external data backup (feature in progress)