SlideShare ist ein Scribd-Unternehmen logo
1 von 33
Downloaden Sie, um offline zu lesen
Clojure@Nuday
Josh Glover, Lead Backend Developer
Nuday Games
● Stockholm-based
game company
● Maker of Rock
Science, “The Rock
Game of the
Century”
Why Clojure?
● See “Hackers and Painters”
● Small dev team can do a lot quickly
● Easier to reason about concurrency
● Engineers attracted to Clojure tend to be
good
● Fits in well with all the battle-tested Java
machinery for service at scale
Architecture
● Frontend (iOS app for now)
● Load balancers
● HTTP servers
● RESTful API
● Databases
Users
DNS round robin
nginx nginx
Elastic Beanstalk
ELB
Tomcat
...
...
SNS
S3
Elastic Beanstalk
Worker
Tomcat
SQS
RDS
Dynamo
DB
RESTful API
● Tomcat 7 on Elastic Beanstalk
● Liberator / Compojure
Routes
(ns insurrection.routes.core
(:require [insurrection.resource.get :as get]
[insurrection.resource.put :as put]))
(defroutes rockscience-routes
(GET "/health" [] get/health)
(GET "/player/:playerId/profile" [] get/profile)
(GET "/player/:playerId/games" [] get/games)
(GET "/player/:playerId/events" [] get/events)
(PUT "/player/:playerId/pushToken" [] put/push-token))
Resources
(ns insurrection.resource.get)
(def events
(get-resource :events view/events [:playerId :timestamp]
:transform-fn {:playerId to-int
:timestamp to-long}
:validation-fn {:playerId (validate
EntityId)
:timestamp (validate
Timestamp)}))
Resources under the hood
(ns insurrection.routes.resources)
(defn get-resource
[resource resource-fn args & {:as opts}]
(-> (lib/resource :allowed-methods [:get]
:available-media-types [json-media-type]
:available-charsets ["UTF-8"]
:authorized? authorized?
:exists? (make-exists? resource resource-fn args opts)
:handle-ok (make-response-handler resource :ok)
:handle-not-found (make-not-found-handler resource args opts))
wrap-keyword-params
wrap-json-params
wrap-json-response))
http://clojure-liberator.github.io/liberator/assets/img/decision-graph.svg
Handling a GET
(defn make-exists?
[resource-name resource-fn args & [opts]]
(fn [ctx]
(let [params-str (params->str ctx args opts)]
(log/info (log-str "Getting %s" resource-name params-str))
(try
(when-let [resource (apply resource-fn (params->values ctx args opts))]
{resource-name resource})
(catch Exception e
(log-error e (log-str "Failed to get %s" resource-name params-str))
{:exception e})))))
Response
(defn make-response-handler
[resource-name & [status]]
{:pre [(s/validate s/Keyword resource-name)
(or-validate HttpStatus status)]}
(let [status (or status :ok)]
(fn [ctx]
(let [exception (:exception ctx)
resource (if exception
{:status false, :message (.getMessage exception)}
(resource-name ctx))
resp (make-response resource)
http-status (if exception
500
(http/status status))]
(ring-response (-> resp (assoc :status http-status)))))))
Types, what are they good for?
● Dynamic typing is wonderful, but…
● Prismatic Schema gives you validation and
documentation without fighting the compiler
Schemas
(ns rockscience.models.entity
(:require [rockscience.rules :as rockscience]
[schema.core :as s])
(def EntityId
(s/both s/Int
(s/pred pos? 'pos?)))
(def Timestamp
(s/both s/Int
(s/pred #(>= % rockscience/epoch) 'ts?)))
Event sourcing
http://martinfowler.com/eaaDev/EventSourcing.html
The fundamental idea of Event Sourcing is that of ensuring
every change to the state of an application is captured in an
event object, and that these event objects are themselves
stored in the sequence they were applied for the same
lifetime as the application state itself.
Querying event log
GET /player/:playerId/events?timestamp=1413801431000
{
"timestamp": "2014-10-20T15:40:47Z",
"events": [
{
"eventType": "ChallengeReceived",
"playerId": "123",
"timestamp": "2014-10-20T11:05:40Z",
"entityId": "912"
},
{
"eventType": "RoundAnswered",
"playerId": "123",
"timestamp":"2014-10-20T11:06:12Z",
"entityId":"912"
},
{
"eventType":"GameEnded",
"playerId":"123",
"timestamp":"2014-10-20T11:09:05Z",
"entityId":"912"
}
]
}
Event log view
(defn events [player-id timestamp]
{:pre [(validate EntityId player-id)
(validate Int timestamp)]
:post [(validate ApiPlayerNotificationEvents %)]}
{:timestamp (->api-timestamp (to-long (now)))
:events (->> (player/get-events player-id timestamp)
(map ->api-player-notification-event))})
More interesting schemas
(def ApiPlayerNotificationEvents
{:timestamp ApiTimestamp
:events [ApiPlayerNotificationEvent]})
(def ApiTimestamp
(s/both s/Str
(s/pred api-timestamp? 'api-timestamp?)))
(defn api-timestamp? [s]
(try (parse (formatters :date-time-no-ms) s)
(catch Exception _ false)))
Schemas built of schemas
(def ApiPlayerNotificationEvent
(->> (merge PlayerNotificationEvent
{:event-type ApiPlayerNotificationEventType
:timestamp ApiTimestamp})
(mmap #(vector (->camelCase %1) %2))))
(def ApiPlayerNotificationEventType
(->> event-types (map ->api-event-type) (apply s/enum)))
(def event-types
#{:challenge-received :round-completed :game-ended})
It’s turtles all the way down
(def PlayerNotificationEvent
{:player-id s/Str
:event-type PlayerNotificationEventType
:entity-id s/Str
:timestamp s/Int})
(def PlayerNotificationEventType
(apply s/enum event-types))
Transmogrify turtles to elephants
(defn ->api-player-notification-event
[ev]
{:pre [(s/validate PlayerNotificationEvent ev)]
:post [(s/validate ApiPlayerNotificationEvent %)]}
(->> ev
(mmap (fn [k v] [(->camelCase k)
(case k
:event-type (->api-event-type v)
:timestamp (->api-timestamp v)
v)]))))
(defn ->api-timestamp
[ts]
{:pre [(s/validate s/Int ts)]
:post [(s/validate ApiTimestamp %)]}
(unparse ts-formatter (from-long ts)))
Persisting events
● Events created on every
mutation, so write fast
● Not really relational
● Good fit for NoSQL!
Specifically, DynamoDB
● Dynamo is scaled for
reads/sec and writes/sec
per table
Events table
(require '[amazonica.aws.dynamodbv2 :as dynamo])
(-> (dynamo/describe-table "events") :table pprint)
;=> {:table-size-bytes 28296,
:item-count 418,
:table-status "ACTIVE",
:creation-date-time #<DateTime 2014-09-17T11:41:47.000+02:00>,
:table-name "events",
:attribute-definitions
[{:attribute-type "S", :attribute-name "player-id"}
{:attribute-type "N", :attribute-name "timestamp"}],
:key-schema
[{:attribute-name "player-id", :key-type "HASH"}
{:attribute-name "timestamp", :key-type "RANGE"}],
:provisioned-throughput
{:number-of-decreases-today 0,
:read-capacity-units 1,
:write-capacity-units 1}}
Querying events
(->> (dynamo/query
:table-name "events"
:select "ALL_ATTRIBUTES"
:key-conditions {:player-id {:attribute-value-list ["123"]
:comparison-operator "EQ"}
:timestamp {:attribute-value-list [1413801431000]
:comparison-operator "GE"}})
:items
first
pprint)
{:event-type "challenge-received",
:player-id "123",
:entity-id "912",
:timestamp 1413803140000}
Writing events
(dynamo/put-item :table-name "events",
:item {:event-type "challenge-received",
:player-id "123",
:entity-id "912",
:timestamp 1413803140000}))
Replaying events
● Our events report on mutation, but don’t
cause it
● Not enough data for replay :(
CQRS?
http://martinfowler.com/bliki/CQRS.html
Ideal mutation flow?
HTTP POST, PUT, or DELETE
Create mutation command
Log mutation command to DynamoDB, return ID
Publish mutation ID to SQS
Receive mutation ID from SQS
Apply mutation command
Update DynamoDB with mutation outcome
HTTP GET /mutation/:id
Update client state
App
REST service
Worker
App
Challenges with this approach
● Our app assumes synchronous requests
● Extra SQS puts and gets introduce more
latency
● Implementing long polling after the fact is a
PITA
● Web sockets to the rescue?
Compromise mutation flow
HTTP POST, PUT, or DELETE
Apply mutation command
Log mutation command to DynamoDB
Return result
Update client state
App
REST service
App
Replaying commands
Read command from DynamoDB
Publish command to SQS
Receive mutation command from SQS
Apply mutation command
If result is different than it was the first time, fail?
Loader
Worker
No CQRS
HTTP POST, PUT, or DELETE
Log mutation model to DynamoDB
Perform mutation
Return result
Update client state
App
REST service
App
Replaying without CQRS
Read mutation model from DynamoDB
HTTP POST, PUT, or DELETE
Loader
Perform mutation
Return result
REST service
If result is different, fail? Loader
When event sourcing, do:
● Ensure that your mutations are idempotent
● Log enough to replay

Weitere ähnliche Inhalte

Was ist angesagt?

Node.js - async for the rest of us.
Node.js - async for the rest of us.Node.js - async for the rest of us.
Node.js - async for the rest of us.Mike Brevoort
 
Roll Your Own API Management Platform with nginx and Lua
Roll Your Own API Management Platform with nginx and LuaRoll Your Own API Management Platform with nginx and Lua
Roll Your Own API Management Platform with nginx and LuaJon Moore
 
Using Node.js to Build Great Streaming Services - HTML5 Dev Conf
Using Node.js to  Build Great  Streaming Services - HTML5 Dev ConfUsing Node.js to  Build Great  Streaming Services - HTML5 Dev Conf
Using Node.js to Build Great Streaming Services - HTML5 Dev ConfTom Croucher
 
Asynchronous Programming FTW! 2 (with AnyEvent)
Asynchronous Programming FTW! 2 (with AnyEvent)Asynchronous Programming FTW! 2 (with AnyEvent)
Asynchronous Programming FTW! 2 (with AnyEvent)xSawyer
 
Asynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time MessagingAsynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time MessagingSteve Rhoades
 
Introduction to Nodejs
Introduction to NodejsIntroduction to Nodejs
Introduction to NodejsGabriele Lana
 
Future Decoded - Node.js per sviluppatori .NET
Future Decoded - Node.js per sviluppatori .NETFuture Decoded - Node.js per sviluppatori .NET
Future Decoded - Node.js per sviluppatori .NETGianluca Carucci
 
Using ngx_lua in UPYUN
Using ngx_lua in UPYUNUsing ngx_lua in UPYUN
Using ngx_lua in UPYUNCong Zhang
 
node.js workshop- node.js basics
node.js workshop- node.js basicsnode.js workshop- node.js basics
node.js workshop- node.js basicsQiong Wu
 
Building Java and Android apps on the blockchain
Building Java and Android apps on the blockchain Building Java and Android apps on the blockchain
Building Java and Android apps on the blockchain Conor Svensson
 
Будь первым
Будь первымБудь первым
Будь первымFDConf
 
"The little big project. From zero to hero in two weeks with 3 front-end engi...
"The little big project. From zero to hero in two weeks with 3 front-end engi..."The little big project. From zero to hero in two weeks with 3 front-end engi...
"The little big project. From zero to hero in two weeks with 3 front-end engi...Fwdays
 
Any event intro
Any event introAny event intro
Any event introqiang
 
node.js workshop- node.js middleware
node.js workshop- node.js middlewarenode.js workshop- node.js middleware
node.js workshop- node.js middlewareQiong Wu
 
Bootstrapping multidc observability stack
Bootstrapping multidc observability stackBootstrapping multidc observability stack
Bootstrapping multidc observability stackBram Vogelaar
 
Puerto serialarduino
Puerto serialarduinoPuerto serialarduino
Puerto serialarduinozadkiel_123
 
ZeroMQ Is The Answer
ZeroMQ Is The AnswerZeroMQ Is The Answer
ZeroMQ Is The AnswerIan Barber
 

Was ist angesagt? (20)

Bash Scripting Workshop
Bash Scripting WorkshopBash Scripting Workshop
Bash Scripting Workshop
 
Node.js - async for the rest of us.
Node.js - async for the rest of us.Node.js - async for the rest of us.
Node.js - async for the rest of us.
 
Socket.io
Socket.ioSocket.io
Socket.io
 
Roll Your Own API Management Platform with nginx and Lua
Roll Your Own API Management Platform with nginx and LuaRoll Your Own API Management Platform with nginx and Lua
Roll Your Own API Management Platform with nginx and Lua
 
Using Node.js to Build Great Streaming Services - HTML5 Dev Conf
Using Node.js to  Build Great  Streaming Services - HTML5 Dev ConfUsing Node.js to  Build Great  Streaming Services - HTML5 Dev Conf
Using Node.js to Build Great Streaming Services - HTML5 Dev Conf
 
Asynchronous Programming FTW! 2 (with AnyEvent)
Asynchronous Programming FTW! 2 (with AnyEvent)Asynchronous Programming FTW! 2 (with AnyEvent)
Asynchronous Programming FTW! 2 (with AnyEvent)
 
Asynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time MessagingAsynchronous PHP and Real-time Messaging
Asynchronous PHP and Real-time Messaging
 
Introduction to Nodejs
Introduction to NodejsIntroduction to Nodejs
Introduction to Nodejs
 
Future Decoded - Node.js per sviluppatori .NET
Future Decoded - Node.js per sviluppatori .NETFuture Decoded - Node.js per sviluppatori .NET
Future Decoded - Node.js per sviluppatori .NET
 
Using ngx_lua in UPYUN
Using ngx_lua in UPYUNUsing ngx_lua in UPYUN
Using ngx_lua in UPYUN
 
node.js workshop- node.js basics
node.js workshop- node.js basicsnode.js workshop- node.js basics
node.js workshop- node.js basics
 
Building Java and Android apps on the blockchain
Building Java and Android apps on the blockchain Building Java and Android apps on the blockchain
Building Java and Android apps on the blockchain
 
Будь первым
Будь первымБудь первым
Будь первым
 
"The little big project. From zero to hero in two weeks with 3 front-end engi...
"The little big project. From zero to hero in two weeks with 3 front-end engi..."The little big project. From zero to hero in two weeks with 3 front-end engi...
"The little big project. From zero to hero in two weeks with 3 front-end engi...
 
Any event intro
Any event introAny event intro
Any event intro
 
Web3j 2.0 Update
Web3j 2.0 UpdateWeb3j 2.0 Update
Web3j 2.0 Update
 
node.js workshop- node.js middleware
node.js workshop- node.js middlewarenode.js workshop- node.js middleware
node.js workshop- node.js middleware
 
Bootstrapping multidc observability stack
Bootstrapping multidc observability stackBootstrapping multidc observability stack
Bootstrapping multidc observability stack
 
Puerto serialarduino
Puerto serialarduinoPuerto serialarduino
Puerto serialarduino
 
ZeroMQ Is The Answer
ZeroMQ Is The AnswerZeroMQ Is The Answer
ZeroMQ Is The Answer
 

Ähnlich wie Clojure at Nuday: How Event Sourcing and Schemas Power Rock Science

Intravert Server side processing for Cassandra
Intravert Server side processing for CassandraIntravert Server side processing for Cassandra
Intravert Server side processing for CassandraEdward Capriolo
 
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"DataStax Academy
 
Declarative & workflow based infrastructure with Terraform
Declarative & workflow based infrastructure with TerraformDeclarative & workflow based infrastructure with Terraform
Declarative & workflow based infrastructure with TerraformRadek Simko
 
FP - Découverte de Play Framework Scala
FP - Découverte de Play Framework ScalaFP - Découverte de Play Framework Scala
FP - Découverte de Play Framework ScalaKévin Margueritte
 
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...Databricks
 
Online Meetup: Why should container system / platform builders care about con...
Online Meetup: Why should container system / platform builders care about con...Online Meetup: Why should container system / platform builders care about con...
Online Meetup: Why should container system / platform builders care about con...Docker, Inc.
 
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...Amazon Web Services
 
If you give a mouse a clickhouse, by Alex Hofsteede, Sentry
If you give a mouse a clickhouse, by Alex Hofsteede, SentryIf you give a mouse a clickhouse, by Alex Hofsteede, Sentry
If you give a mouse a clickhouse, by Alex Hofsteede, SentryAltinity Ltd
 
FwDays 2021: Metarhia Technology Stack for Node.js
FwDays 2021: Metarhia Technology Stack for Node.jsFwDays 2021: Metarhia Technology Stack for Node.js
FwDays 2021: Metarhia Technology Stack for Node.jsTimur Shemsedinov
 
Introduction to WSO2 Data Analytics Platform
Introduction to  WSO2 Data Analytics PlatformIntroduction to  WSO2 Data Analytics Platform
Introduction to WSO2 Data Analytics PlatformSrinath Perera
 
soft-shake.ch - Hands on Node.js
soft-shake.ch - Hands on Node.jssoft-shake.ch - Hands on Node.js
soft-shake.ch - Hands on Node.jssoft-shake.ch
 
Protractor framework – how to make stable e2e tests for Angular applications
Protractor framework – how to make stable e2e tests for Angular applicationsProtractor framework – how to make stable e2e tests for Angular applications
Protractor framework – how to make stable e2e tests for Angular applicationsLudmila Nesvitiy
 
The never changing face of immutability
The never changing face of immutabilityThe never changing face of immutability
The never changing face of immutabilityChris Howe-Jones
 
Writing robust Node.js applications
Writing robust Node.js applicationsWriting robust Node.js applications
Writing robust Node.js applicationsTom Croucher
 
Event-driven IO server-side JavaScript environment based on V8 Engine
Event-driven IO server-side JavaScript environment based on V8 EngineEvent-driven IO server-side JavaScript environment based on V8 Engine
Event-driven IO server-side JavaScript environment based on V8 EngineRicardo Silva
 
Querying Data Pipeline with AWS Athena
Querying Data Pipeline with AWS AthenaQuerying Data Pipeline with AWS Athena
Querying Data Pipeline with AWS AthenaYaroslav Tkachenko
 
WSO2Con EU 2016: An Introduction to the WSO2 Analytics Platform
WSO2Con EU 2016: An Introduction to the WSO2 Analytics PlatformWSO2Con EU 2016: An Introduction to the WSO2 Analytics Platform
WSO2Con EU 2016: An Introduction to the WSO2 Analytics PlatformWSO2
 
Deep Dive into Zone.JS
Deep Dive into Zone.JSDeep Dive into Zone.JS
Deep Dive into Zone.JSIlia Idakiev
 

Ähnlich wie Clojure at Nuday: How Event Sourcing and Schemas Power Rock Science (20)

Intravert Server side processing for Cassandra
Intravert Server side processing for CassandraIntravert Server side processing for Cassandra
Intravert Server side processing for Cassandra
 
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"
NYC* 2013 - "Advanced Data Processing: Beyond Queries and Slices"
 
Declarative & workflow based infrastructure with Terraform
Declarative & workflow based infrastructure with TerraformDeclarative & workflow based infrastructure with Terraform
Declarative & workflow based infrastructure with Terraform
 
FP - Découverte de Play Framework Scala
FP - Découverte de Play Framework ScalaFP - Découverte de Play Framework Scala
FP - Découverte de Play Framework Scala
 
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...
Designing and Implementing a Real-time Data Lake with Dynamically Changing Sc...
 
Online Meetup: Why should container system / platform builders care about con...
Online Meetup: Why should container system / platform builders care about con...Online Meetup: Why should container system / platform builders care about con...
Online Meetup: Why should container system / platform builders care about con...
 
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...
(BDT401) Big Data Orchestra - Harmony within Data Analysis Tools | AWS re:Inv...
 
How to React Native
How to React NativeHow to React Native
How to React Native
 
If you give a mouse a clickhouse, by Alex Hofsteede, Sentry
If you give a mouse a clickhouse, by Alex Hofsteede, SentryIf you give a mouse a clickhouse, by Alex Hofsteede, Sentry
If you give a mouse a clickhouse, by Alex Hofsteede, Sentry
 
dSS API by example
dSS API by exampledSS API by example
dSS API by example
 
FwDays 2021: Metarhia Technology Stack for Node.js
FwDays 2021: Metarhia Technology Stack for Node.jsFwDays 2021: Metarhia Technology Stack for Node.js
FwDays 2021: Metarhia Technology Stack for Node.js
 
Introduction to WSO2 Data Analytics Platform
Introduction to  WSO2 Data Analytics PlatformIntroduction to  WSO2 Data Analytics Platform
Introduction to WSO2 Data Analytics Platform
 
soft-shake.ch - Hands on Node.js
soft-shake.ch - Hands on Node.jssoft-shake.ch - Hands on Node.js
soft-shake.ch - Hands on Node.js
 
Protractor framework – how to make stable e2e tests for Angular applications
Protractor framework – how to make stable e2e tests for Angular applicationsProtractor framework – how to make stable e2e tests for Angular applications
Protractor framework – how to make stable e2e tests for Angular applications
 
The never changing face of immutability
The never changing face of immutabilityThe never changing face of immutability
The never changing face of immutability
 
Writing robust Node.js applications
Writing robust Node.js applicationsWriting robust Node.js applications
Writing robust Node.js applications
 
Event-driven IO server-side JavaScript environment based on V8 Engine
Event-driven IO server-side JavaScript environment based on V8 EngineEvent-driven IO server-side JavaScript environment based on V8 Engine
Event-driven IO server-side JavaScript environment based on V8 Engine
 
Querying Data Pipeline with AWS Athena
Querying Data Pipeline with AWS AthenaQuerying Data Pipeline with AWS Athena
Querying Data Pipeline with AWS Athena
 
WSO2Con EU 2016: An Introduction to the WSO2 Analytics Platform
WSO2Con EU 2016: An Introduction to the WSO2 Analytics PlatformWSO2Con EU 2016: An Introduction to the WSO2 Analytics Platform
WSO2Con EU 2016: An Introduction to the WSO2 Analytics Platform
 
Deep Dive into Zone.JS
Deep Dive into Zone.JSDeep Dive into Zone.JS
Deep Dive into Zone.JS
 

Clojure at Nuday: How Event Sourcing and Schemas Power Rock Science

  • 2. Nuday Games ● Stockholm-based game company ● Maker of Rock Science, “The Rock Game of the Century”
  • 3. Why Clojure? ● See “Hackers and Painters” ● Small dev team can do a lot quickly ● Easier to reason about concurrency ● Engineers attracted to Clojure tend to be good ● Fits in well with all the battle-tested Java machinery for service at scale
  • 4. Architecture ● Frontend (iOS app for now) ● Load balancers ● HTTP servers ● RESTful API ● Databases
  • 5. Users DNS round robin nginx nginx Elastic Beanstalk ELB Tomcat ... ... SNS S3 Elastic Beanstalk Worker Tomcat SQS RDS Dynamo DB
  • 6. RESTful API ● Tomcat 7 on Elastic Beanstalk ● Liberator / Compojure
  • 7. Routes (ns insurrection.routes.core (:require [insurrection.resource.get :as get] [insurrection.resource.put :as put])) (defroutes rockscience-routes (GET "/health" [] get/health) (GET "/player/:playerId/profile" [] get/profile) (GET "/player/:playerId/games" [] get/games) (GET "/player/:playerId/events" [] get/events) (PUT "/player/:playerId/pushToken" [] put/push-token))
  • 8. Resources (ns insurrection.resource.get) (def events (get-resource :events view/events [:playerId :timestamp] :transform-fn {:playerId to-int :timestamp to-long} :validation-fn {:playerId (validate EntityId) :timestamp (validate Timestamp)}))
  • 9. Resources under the hood (ns insurrection.routes.resources) (defn get-resource [resource resource-fn args & {:as opts}] (-> (lib/resource :allowed-methods [:get] :available-media-types [json-media-type] :available-charsets ["UTF-8"] :authorized? authorized? :exists? (make-exists? resource resource-fn args opts) :handle-ok (make-response-handler resource :ok) :handle-not-found (make-not-found-handler resource args opts)) wrap-keyword-params wrap-json-params wrap-json-response)) http://clojure-liberator.github.io/liberator/assets/img/decision-graph.svg
  • 10. Handling a GET (defn make-exists? [resource-name resource-fn args & [opts]] (fn [ctx] (let [params-str (params->str ctx args opts)] (log/info (log-str "Getting %s" resource-name params-str)) (try (when-let [resource (apply resource-fn (params->values ctx args opts))] {resource-name resource}) (catch Exception e (log-error e (log-str "Failed to get %s" resource-name params-str)) {:exception e})))))
  • 11. Response (defn make-response-handler [resource-name & [status]] {:pre [(s/validate s/Keyword resource-name) (or-validate HttpStatus status)]} (let [status (or status :ok)] (fn [ctx] (let [exception (:exception ctx) resource (if exception {:status false, :message (.getMessage exception)} (resource-name ctx)) resp (make-response resource) http-status (if exception 500 (http/status status))] (ring-response (-> resp (assoc :status http-status)))))))
  • 12. Types, what are they good for? ● Dynamic typing is wonderful, but… ● Prismatic Schema gives you validation and documentation without fighting the compiler
  • 13. Schemas (ns rockscience.models.entity (:require [rockscience.rules :as rockscience] [schema.core :as s]) (def EntityId (s/both s/Int (s/pred pos? 'pos?))) (def Timestamp (s/both s/Int (s/pred #(>= % rockscience/epoch) 'ts?)))
  • 14. Event sourcing http://martinfowler.com/eaaDev/EventSourcing.html The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.
  • 15. Querying event log GET /player/:playerId/events?timestamp=1413801431000 { "timestamp": "2014-10-20T15:40:47Z", "events": [ { "eventType": "ChallengeReceived", "playerId": "123", "timestamp": "2014-10-20T11:05:40Z", "entityId": "912" }, { "eventType": "RoundAnswered", "playerId": "123", "timestamp":"2014-10-20T11:06:12Z", "entityId":"912" }, { "eventType":"GameEnded", "playerId":"123", "timestamp":"2014-10-20T11:09:05Z", "entityId":"912" } ] }
  • 16. Event log view (defn events [player-id timestamp] {:pre [(validate EntityId player-id) (validate Int timestamp)] :post [(validate ApiPlayerNotificationEvents %)]} {:timestamp (->api-timestamp (to-long (now))) :events (->> (player/get-events player-id timestamp) (map ->api-player-notification-event))})
  • 17. More interesting schemas (def ApiPlayerNotificationEvents {:timestamp ApiTimestamp :events [ApiPlayerNotificationEvent]}) (def ApiTimestamp (s/both s/Str (s/pred api-timestamp? 'api-timestamp?))) (defn api-timestamp? [s] (try (parse (formatters :date-time-no-ms) s) (catch Exception _ false)))
  • 18. Schemas built of schemas (def ApiPlayerNotificationEvent (->> (merge PlayerNotificationEvent {:event-type ApiPlayerNotificationEventType :timestamp ApiTimestamp}) (mmap #(vector (->camelCase %1) %2)))) (def ApiPlayerNotificationEventType (->> event-types (map ->api-event-type) (apply s/enum))) (def event-types #{:challenge-received :round-completed :game-ended})
  • 19. It’s turtles all the way down (def PlayerNotificationEvent {:player-id s/Str :event-type PlayerNotificationEventType :entity-id s/Str :timestamp s/Int}) (def PlayerNotificationEventType (apply s/enum event-types))
  • 20. Transmogrify turtles to elephants (defn ->api-player-notification-event [ev] {:pre [(s/validate PlayerNotificationEvent ev)] :post [(s/validate ApiPlayerNotificationEvent %)]} (->> ev (mmap (fn [k v] [(->camelCase k) (case k :event-type (->api-event-type v) :timestamp (->api-timestamp v) v)])))) (defn ->api-timestamp [ts] {:pre [(s/validate s/Int ts)] :post [(s/validate ApiTimestamp %)]} (unparse ts-formatter (from-long ts)))
  • 21. Persisting events ● Events created on every mutation, so write fast ● Not really relational ● Good fit for NoSQL! Specifically, DynamoDB ● Dynamo is scaled for reads/sec and writes/sec per table
  • 22. Events table (require '[amazonica.aws.dynamodbv2 :as dynamo]) (-> (dynamo/describe-table "events") :table pprint) ;=> {:table-size-bytes 28296, :item-count 418, :table-status "ACTIVE", :creation-date-time #<DateTime 2014-09-17T11:41:47.000+02:00>, :table-name "events", :attribute-definitions [{:attribute-type "S", :attribute-name "player-id"} {:attribute-type "N", :attribute-name "timestamp"}], :key-schema [{:attribute-name "player-id", :key-type "HASH"} {:attribute-name "timestamp", :key-type "RANGE"}], :provisioned-throughput {:number-of-decreases-today 0, :read-capacity-units 1, :write-capacity-units 1}}
  • 23. Querying events (->> (dynamo/query :table-name "events" :select "ALL_ATTRIBUTES" :key-conditions {:player-id {:attribute-value-list ["123"] :comparison-operator "EQ"} :timestamp {:attribute-value-list [1413801431000] :comparison-operator "GE"}}) :items first pprint) {:event-type "challenge-received", :player-id "123", :entity-id "912", :timestamp 1413803140000}
  • 24. Writing events (dynamo/put-item :table-name "events", :item {:event-type "challenge-received", :player-id "123", :entity-id "912", :timestamp 1413803140000}))
  • 25. Replaying events ● Our events report on mutation, but don’t cause it ● Not enough data for replay :(
  • 27. Ideal mutation flow? HTTP POST, PUT, or DELETE Create mutation command Log mutation command to DynamoDB, return ID Publish mutation ID to SQS Receive mutation ID from SQS Apply mutation command Update DynamoDB with mutation outcome HTTP GET /mutation/:id Update client state App REST service Worker App
  • 28. Challenges with this approach ● Our app assumes synchronous requests ● Extra SQS puts and gets introduce more latency ● Implementing long polling after the fact is a PITA ● Web sockets to the rescue?
  • 29. Compromise mutation flow HTTP POST, PUT, or DELETE Apply mutation command Log mutation command to DynamoDB Return result Update client state App REST service App
  • 30. Replaying commands Read command from DynamoDB Publish command to SQS Receive mutation command from SQS Apply mutation command If result is different than it was the first time, fail? Loader Worker
  • 31. No CQRS HTTP POST, PUT, or DELETE Log mutation model to DynamoDB Perform mutation Return result Update client state App REST service App
  • 32. Replaying without CQRS Read mutation model from DynamoDB HTTP POST, PUT, or DELETE Loader Perform mutation Return result REST service If result is different, fail? Loader
  • 33. When event sourcing, do: ● Ensure that your mutations are idempotent ● Log enough to replay