Harlan Iverson vor 9 Jahren
Ursprung
Commit
8fe8fab5df
1 geänderte Dateien mit 389 neuen und 8 gelöschten Zeilen
  1. 389 8
      README.md

+ 389 - 8
README.md

@@ -11,11 +11,11 @@ A baseline Clojure stack that runs on any and all platforms, that is nice to dev
 ## What's here?
 
 * Leiningen based multi-module project setup with modules for each platform
-  * API via bidi with REPL control
-  * Web via clojurescript / re-frame
-  * iOS via clojure-objc / lein-objcbuild
-  * Androd via clojure-android / lein-droid
-  * Core via component
+    * API via bidi with REPL control
+    * Web via clojurescript / re-frame
+    * iOS via clojure-objc / lein-objcbuild
+    * Androd via clojure-android / lein-droid
+    * Core via component
 * Integration with Vagrant and Marathon
 * Scalable production deploment configuration
 * Development environment mirroring production environment
@@ -25,6 +25,134 @@ A baseline Clojure stack that runs on any and all platforms, that is nice to dev
 
 ## Philosophy
 
+Much influence has been drawn from Continuous Delivery, The Reactive Manifesto, and the Lean Startup. Talks such as 
+[Turning the Database Inside out]() and ___ have also had large influence.
+
+
+### Aggregate Objects
+
+After two years of clojure and data stream programming, one begins to think of all objects as aggregates of the
+events that it they have experienced. What is a key/value store? A sequence of keys and values. How can we build one?
+
+1. `(write {:key "harlan.name.first" :value "Harlan"})`, we'll use `[harlan.name.first Harlan]` for brevity.
+2. `[harlan.name.last Iverson]`
+3. `[harlan.job Hair Stylist]`
+4. `[alice.name.first Alice]`
+5. `[alice.name.last Simply]`
+6. `[alice.job Programmer]`
+
+```
+{"harlan.name.first" "Harlan"
+ "harlan.name.last" "Iverson"
+ "harlan.job" "Hair Stylist"
+ "alice.name.first" "Alice"
+ "alice.name.last" "Simply"
+ "alice.job" "Programmer"}
+```
+
+7. `[harlan.job Programmer]`
+
+```
+{"harlan.name.first" "Harlan"
+ "harlan.name.last" "Iverson"
+ "alice.name.first" "Alice"
+ "alice.name.last" "Simply"
+ "alice.job" "Programmer"
+ "harlan.job" "Programmer"}
+```
+
+The rule here is to have one slot per key that can hold a value, and keep the latest one we receive; remember, 
+these are write events. Thankfully Computer Science gave us the Dictionary data structure that we can leverage:
+
+```clojure
+(def assoc clojure.core/assoc) ; not needed, just for example. works on dictionaries, ka maps.
+
+(defn kv-store []
+  (let [store (atom {})]
+    (fn [event] (swap! store assoc (:key event) (:value event))))
+```
+
+We can image more interesting objects, like summation or moving average. I am using objects more in the logic sense than
+the OOP sense.
+
+```clojure
+(defn- inc [value] (+ 1 (or value 0)))
+
+(defn sum []
+  (let [value (atom 0)]
+    (fn [event] (swap! value inc)))
+```
+
+```clojure
+(defn- cma-pair [[i avg] value]
+  (let [new-i (+ 1 i)
+        new-avg (/ (+ avg value) new-i)]
+    [new-i new-avg]))
+
+(defn cma []
+   (let [state (atom [0 0])]
+     (fn [event] (second (swap! state update cma-pair (:value event))))))
+```
+
+
+These are foundational events. Let's try something more advanced:
+
+
+```clojure
+(defn cma []
+   (let [state (atom [0 0])]
+     (fn [event] (second (swap! state update cma-pair (:value event))))))
+```
+
+
+
+
+Stream Middleware.
+
+https://github.com/pyr/riemann-kafka
+
+Riemann + Kafka...
+
+
+http://allthingshadoop.com/2014/04/18/metrics-kafka/
+
+
+
+
+
+These aggregate objects can be composed using Clojure's inbuilt `(comp)` function.
+
+```
+;
+
+(defrecord EventsByCountry [users number-of-users]
+  (init []
+    {:users (atom (or users (kv-store)))
+     :number-of-users (atom (or number-of-users (count)))})
+  (process [forward events event]
+    (swap!  )))
+
+(let [users (kv-store)
+      number-of-users (count)]
+  (do (users event)
+      (number-of-users event)))
+```
+
+
+They can then be placed in pipelines as xforms on core.async channels.
+
+```
+;
+
+(def input (chan))
+
+(def 
+
+
+```
+
+
+
 ### Claims
 
 
@@ -36,6 +164,7 @@ A baseline Clojure stack that runs on any and all platforms, that is nice to dev
 * EDN minimally represents all fundamental data structures; the end game of text formats
 * Clojure is easy to read and understand, and is not quite LISP (it's EDN)
 * Investment in Open Source Software leads to better software product outcomes
+* Dependencies between features should be minimized, yet code sharing should be maximized and DRY
 
 
 ## Choices
@@ -48,7 +177,69 @@ The fundamental data structures representing code and data; nothing more, nothin
 * core.async channels and CSP concurrency everywhere
 * EDN everywhere, successor to JSON
 
-#### re-frame
+#### core.async
+
+Core.async provides buffered and unbuffered channels and macros to enable CSP concurrency, effectively making all arguments
+for Go apply to Clojure in addition to the tooling and library support of the JVM. Additionally there is full integration
+with transducers, allowing for composable and high performance computation (low gc pressure via stack). If we write all
+components as transducers or reducers, we can run code anywhere and wire it together with core.async channels.
+
+```clojure
+(defn assoc-profile []
+  (fn [event next]
+    (let [profile (profiles (:myapp/userid event))
+          new-event (if profile
+                        (assoc event :profile profile)
+                        event)]
+      (next new-event))))
+```
+
+```clojure
+(defn allow-language [lang]
+  (fn [event next]
+    (when (= (:myapp/lang event) lang)
+      (next event))))
+```
+
+
+```clojure
+
+(defn kv-store 
+  ([key-fn val-fn]
+     (kv-store key-fn val-fn (atom {}))
+  ([key-fn val-fn kv-atom]
+     (fn [event next]
+       (let [k (key-fn event)
+             v (val-fn event)]
+         (next (case v
+                     ::delete (swap! kv-atom disassoc k)
+                     ; if v is a list then the first will be the function and the rest the args.
+                     ; eg. 
+                     ; {:component/event :kv-store/update
+                     ;  :kv-store/key 123
+                     ;  :kv-store/update-fn '(+ (:total-price event) (or prev 0))}
+                     ::update (let [update-fn (eval (:kv-store/update-fn update-fn))]
+                                (swap! kv-atom update k update-fn event))
+                     :else (swap! kv-atom assoc k v)))))
+                     
+                     
+(def latest-value kv-store) ; alias
+
+(defn nearby [user-id other-user-id max-distance] (fn [event] (and (not= (:user-id %) user-id) (>= max-distance (distance 
+
+(defn users-nearby [users]
+  (fn [event next]
+    (let [user-id (:user-id event)
+          location (:location event)]
+      (swap! users assoc user-id location)
+      (let [nearby-users (into [] (comp (filter (fn [[k v]]
+                                                (when (distance )))])))
+
+
+```
+
+
+#### reagent
 
 Based on reagent, which using a special atom as the client side sate. Stacks a nice pattern for composable reactive
 processing of UI actions. Plays well with CQRS and Doman Driven Design.
@@ -61,7 +252,15 @@ A separation of concerns model for use on all platforms.
 We use a custom lifecycle dispatcher that uses multimethods, as suggested by the author's documentation.
 
 Actually this style [is much slower](http://insideclojure.org/2015/04/27/poly-perf/) than Protocol based dispatch, 
-but very nice to work with.
+but very nice to work with. [Or not](http://stackoverflow.com/questions/28577115/performance-of-multimethod-vs-cond-in-clojure).
+
+```
+Multimethods: 6.26 ms
+Cond-itionnal: 5.18 ms
+Protocols: 6.04 ms
+```
+
+
 
 ```clojure
 (ns my.component
@@ -91,6 +290,7 @@ but very nice to work with.
 A comparable protocol-based approach loses some of its niceness, but may win in dispatch speed. A macro could be constructed
 to use an in-memory map based dispatch (ie. constant lookup time).
 
+
 ```clojure
 (ns my.component
   (:require [clojure.tools.logging :refer [debugf infof]
@@ -115,15 +315,67 @@ to use an in-memory map based dispatch (ie. constant lookup time).
 (def component (->Reactor {::my-message my-message}))
 
 ;(react component event)
-
 ```
 
 
 ### Marathon deployment
 
+12 Factor App
+
 Deploy non-Docker packages. Nothing in particular against Docker, but I don't see the case for it with Marathon 
 and debootstrap.
 
+Marathon gives the concept of [Application Groups](https://mesosphere.github.io/marathon/docs/application-groups.html), which inject environmental variables into
+containers which con be used for discovery.
+
+
+> Applications can be nested into a n-ary tree, with groups as branches and applications as leaves. Application Groups are used to partition multiple applications into manageable sets.
+
+
+* *kafka*
+* notes service - kafka
+* web app - notes service
+* ios service - notes service
+* android service - notes service
+* ios app - *app store*
+* android app - *android market*
+
+This is the fully expanded view. Initially you may have a single service for all apps, but it is recommended
+to at-least create distinct routes for each type of view. The reason for this is to minimize dependencies. Italic
+means external / not explicitly deployed with this app group.
+
+
+```
+{"id": "notes.prod",
+ "apps": [{"id": "notes.notes-service.prod",
+           "command": "ENV=prod KAFKA=kafka:9092 java -jar notes-service.jar"},
+          {"id": "notes.web.prod",
+           "command": "ENV=prod NOTES_SERVICE=notes.notes-service.prod java -jar notes-web-standalone.jar",
+           "dependencies": ["notes.notes-service.prod"]},
+          {"id": "notes.ios.prod",
+           "command": "ENV=prod NOTES_SERVICE=notes.notes-service.prod java -jar notes-ios-standalone.jar",
+           "dependencies": ["notes.notes-service.prod"]},
+          {"id": "notes.android.prod",
+           "command": "ENV=prod NOTES_SERVICE=notes.notes-service.prod java -jar notes-android-standalone.jar",
+           "dependencies": ["notes.notes-service.prod"]}
+          ]}
+```
+
+#### Mesos DNS (service discovery)
+
+http://mesosphere.github.io/mesos-dns/
+
+Using this, DNS names have a convention based on the container ID. We could thus be implicit about DNS names of
+services, but it is better practice to pass them in explicitly at deployment/run time for separation of concerns.
+
+
+#### Backend per view
+
+This means that we have an endpoint for iOS, Android, Web. Core logic is required by each, but the view details
+may vary slightly if only in deployment schedule.
+
+
+
 ### Ansible
 
 familiar with it. yaml basaed DSL. as simple as you want it to be. downside? not edn.
@@ -133,6 +385,25 @@ familiar with it. yaml basaed DSL. as simple as you want it to be. downside? not
 APT
 debootstrap
 
+We can bootstrap our deployments down to the Kernel level if need-be, but ideally we're deploying to an ubuntu
+system that has all of our dependencies such as a JDK provisioned by Ansible or Packer.
+
+A `.deb` gives us system-level instructions for how to install a package. It may contain upstart info, or other
+ways to invoke the services it contains. Given that container systems want an entry point, that functionality
+is largely un-used. One could interpret the upstart init script to build a command to run.
+
+
+
+### Kafka
+
+Kafka is a replicated append-only log service that works in sergments of key/value messages. The key can serve as
+a primary key with compacted storage or a partition key to balance work amongst subscribers. In our case it is the
+primary data store from which our aggregate state is built.
+
+It serves as a persistent store and buffer against back-pressure and cascading failures.
+
+
+
 ## Tool suggestions
 
 IntelliJ Idea with Cursive (free licenses for OSS!)
@@ -146,3 +417,113 @@ homebrew + cask
 ## Thanks
 
 Thanks to all the authors and contributors of the projects used and literature referenced.
+
+
+
+## Different types of compnents
+
+Think of a component as a binary, and channels as standard in/out. Some binaries run on servers, others on phones, others 
+on desktop computers, etc. Each has different capabilities, a common one being POSIX. When you write a component in Clojre,
+you think in terms of the capabilities of a system. Component helps us manage these.
+
+
+```clojure
+
+(defn example-system [config-options]
+  (let [{:keys [host port]} config-options]
+    (component/system-map
+      :config-options config-options
+      :pubsub (pubsub (:pubsub-upstream config-options))
+      :ga (google-analytics (:ga-config config-options))
+      :app (component/using
+             (ios-client config-options)
+             {:database  :db
+              :scheduler :scheduler}))))
+              
+(def system (component/start (example-system {}))
+
+
+
+
+```
+
+
+
+Idea:
+
+- routing for subkey balancing -- atomic in ZK w/ zk-atom
+- browser creates atom that is materialized from upstream
+- synchronize atom into r/atom (perf of calculating update?)
+- atom over redis/memcache? -- if you need this, you might be designing wrong. kafka is master, partition is everything.
+- subscribe with a predicate, channel is a special case. #(= (:channel %) "my-channel")
+
+
+```clojure
+(defn my-thing [main-ui] [:div "regular reagent component"])
+
+(defrecord MainUI [loc]
+  c/Lifecycle
+  (start [c]
+     (r/render-component (my-thing c) loc))
+  (stop [c] 
+     (comment "cleanup")))
+
+(let [ps (pubsub ...)
+      events-upstream (subscribe ps #(= (:channel %) "my-channel")) ; a channel
+      events (pipe events (r/atom) false) ; pipe returns "to" channel
+      system-map (c/system-map {:ps ps :events events})]
+  (reset! system (c/start system-map))
+```
+
+
+```clojure
+(defn new-session [req]
+  (comment "create kafka consumer subsription, pipe updates through reactor and out to client."))
+
+(defn component-handler [req]
+  (let [input-chan (pipe (http-channel req) (new-session req) false)]))
+
+(defrecord NginxAppServer [port]
+  c/Lifecycle
+  (start [c]
+    (http-)
+  (stop [c])
+  
+  WithRingApp
+  (app [c]
+    (let [handler (comment "normal ring handler, middleware etc")]
+      handler)))
+```
+
+
+
+```clojure
+(defrecord Subscription [in out predicate])
+
+(def subscriptions (atom {})) ; this could be a zk-atom... = coordination among peers >:]
+
+(defn publish [req]
+  (kafka/publish ...))
+
+(defn subscribe [req]
+  (swap! subscriptions assoc sub-id (->Subscription ...))
+  
+(defn history [req]
+  (let [range (comment "offset, count from request")]
+    (comment "git history from requested offset))
+
+(defn unsubscribe [req])
+
+(defn update-consumers [watch-key consumers old new]
+  (let [[old-subscriptions new-subscriptions common-subs] (clojure.data/diff old new)
+        to-add (comment "find add from diff")
+        to-remote (comment "find remove from diff")]
+    (doseq [to-add]
+      )
+    (doseq [to-remove]
+      )))
+
+; keep kafka consumers in sync in a decoupled way :)
+(add-watch subscriptions :update-consumers update-consumers)
+
+```