Browse Source

add ocaml opium (#6061)

* final fixes

* add fortunes and updates

* Wen -> Wed

Co-authored-by: Ryan Slade <[email protected]>

Co-authored-by: Ryan Slade <[email protected]>
mudrz 4 years ago
parent
commit
28227b0fb7

+ 4 - 0
frameworks/OCaml/.gitignore

@@ -0,0 +1,4 @@
+**/_opam
+**/_build
+.merlin
+.ocamlformat

+ 4 - 0
frameworks/OCaml/opium/.dockerignore

@@ -0,0 +1,4 @@
+**/_build/
+**/_opam/
+**/.merlin
+**/.ocamlformat

+ 44 - 0
frameworks/OCaml/opium/README.md

@@ -0,0 +1,44 @@
+# Opium Benchmarking Test
+
+### Test Type Implementation Source Code
+
+* [PLAINTEXT](src/lib/routes.ml#L3-5)
+* [JSON](src/lib/routes.ml#L7-10)
+* [DB](src/lib/routes.ml#L12-L20)
+* [QUERY](src/lib/routes.ml#L37-L41)
+* [UPDATES](src/lib/routes.ml#L43-L56)
+* [FORTUNES](src/lib/routes.ml#L58-L67)
+
+## Important Libraries
+* [Opium](https://github.com/rgrinberg/opium)
+* [Httpaf](https://github.com/inhabitedtype/httpaf)
+* [Caqti](https://github.com/paurkedal/ocaml-caqti)
+* [Lwt](https://github.com/ocsigen/lwt)
+* [Yojson](https://github.com/ocaml-community/yojson)
+* [Ppx_rapper](https://github.com/roddyyaga/ppx_rapper)
+
+## Test URLs
+
+### PLAINTEXT
+
+http://localhost:8080/plaintext
+
+### JSON
+
+http://localhost:8080/json
+
+### DB
+
+http://localhost:8080/db
+
+### QUERY
+
+http://localhost:8080/queries/
+
+### UPDATES
+
+http://localhost:8080/updates/
+
+### FORTUNES
+
+http://localhost:8080/fortunes/

+ 51 - 0
frameworks/OCaml/opium/benchmark_config.json

@@ -0,0 +1,51 @@
+{
+  "framework": "opium",
+  "tests": [{
+      "default": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries/",
+        "update_url": "/updates/",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "opium",
+        "language": "OCaml",
+        "flavor": "None",
+        "orm": "Micro",
+        "platform": "httpaf",
+        "webserver": "None",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "opium",
+        "notes": "",
+        "versus": "None"
+      },
+      "haproxy": {
+        "json_url": "/json",
+        "plaintext_url": "/plaintext",
+        "db_url": "/db",
+        "query_url": "/queries/",
+        "update_url": "/updates/",
+        "fortune_url": "/fortunes",
+        "port": 8080,
+        "approach": "Realistic",
+        "classification": "Micro",
+        "database": "postgres",
+        "framework": "opium",
+        "language": "OCaml",
+        "flavor": "None",
+        "orm": "Micro",
+        "platform": "httpaf",
+        "webserver": "haproxy",
+        "os": "Linux",
+        "database_os": "Linux",
+        "display_name": "opium-haproxy",
+        "notes": "",
+        "versus": "None"
+      }
+  }]
+}

+ 59 - 0
frameworks/OCaml/opium/haproxy.cfg

@@ -0,0 +1,59 @@
+global
+    chroot      /var/lib/haproxy
+    pidfile     /var/run/haproxy.pid
+    maxconn     32768
+    user        haproxy
+    group       haproxy
+    nbthread    2
+
+defaults
+    mode                    tcp
+    log                     global
+    option                  dontlognull
+    option http-server-close
+    option forwardfor       except 127.0.0.0/8
+    option                  redispatch
+    retries                 3
+    timeout http-request    10s
+    timeout queue           1m
+    timeout connect         10s
+    timeout client          1m
+    timeout server          1m
+    timeout http-keep-alive 10s
+    timeout check           10s
+    maxconn                 3000
+
+frontend main
+    bind *:8080
+    default_backend             tfb
+
+backend tfb
+    balance     static-rr
+    server  tfb0 127.0.0.1:9000 check
+    server  tfb1 127.0.0.1:9001 check
+    server  tfb2 127.0.0.1:9002 check
+    server  tfb3 127.0.0.1:9003 check
+    server  tfb4 127.0.0.1:9004 check
+    server  tfb5 127.0.0.1:9005 check
+    server  tfb6 127.0.0.1:9006 check
+    server  tfb7 127.0.0.1:9007 check
+    server  tfb8 127.0.0.1:9008 check
+    server  tfb9 127.0.0.1:9009 check
+    server  tfb10 127.0.0.1:9010 check
+    server  tfb11 127.0.0.1:9011 check
+    server  tfb12 127.0.0.1:9012 check
+    server  tfb13 127.0.0.1:9013 check
+    server  tfb14 127.0.0.1:9014 check
+    server  tfb15 127.0.0.1:9015 check
+    server  tfb16 127.0.0.1:9016 check
+    server  tfb17 127.0.0.1:9017 check
+    server  tfb18 127.0.0.1:9018 check
+    server  tfb19 127.0.0.1:9019 check
+    server  tfb20 127.0.0.1:9020 check
+    server  tfb21 127.0.0.1:9021 check
+    server  tfb22 127.0.0.1:9022 check
+    server  tfb23 127.0.0.1:9023 check
+    server  tfb24 127.0.0.1:9024 check
+    server  tfb25 127.0.0.1:9025 check
+    server  tfb26 127.0.0.1:9026 check
+    server  tfb27 127.0.0.1:9027 check

+ 27 - 0
frameworks/OCaml/opium/opium-haproxy.dockerfile

@@ -0,0 +1,27 @@
+FROM ocurrent/opam:fedora-32-ocaml-4.11
+
+ENV DIR web
+# https://caml.inria.fr/pub/docs/manual-ocaml/libref/Gc.html
+# https://linux.die.net/man/1/ocamlrun
+# https://blog.janestreet.com/memory-allocator-showdown/
+ENV OCAMLRUNPARAM a=2,o=240
+
+RUN sudo dnf install --assumeyes postgresql-devel libev-devel libffi-devel
+
+WORKDIR /${DIR}
+
+COPY src/opi.opam src/Makefile ./
+
+RUN make install-ci
+
+COPY ./src ./
+
+RUN sudo chown -R opam: . && make build
+
+# try to keep everything above here in sync with other dockerfiles in project for more efficent use of docker build cache
+RUN sudo dnf install --assumeyes haproxy
+COPY haproxy.cfg /etc/haproxy/haproxy.cfg
+COPY start-servers.sh ./start-servers.sh
+RUN sudo chown -R opam: /${DIR} && chmod +x ./start-servers.sh
+
+CMD ./start-servers.sh && sudo /usr/sbin/haproxy -W -f /etc/haproxy/haproxy.cfg -p /run/haproxy.pid -q

+ 21 - 0
frameworks/OCaml/opium/opium.dockerfile

@@ -0,0 +1,21 @@
+FROM ocurrent/opam:fedora-32-ocaml-4.11
+
+ENV DIR web
+# https://caml.inria.fr/pub/docs/manual-ocaml/libref/Gc.html
+# https://linux.die.net/man/1/ocamlrun
+# https://blog.janestreet.com/memory-allocator-showdown/
+ENV OCAMLRUNPARAM a=2,o=240
+
+RUN sudo dnf install --assumeyes postgresql-devel libev-devel libffi-devel
+
+WORKDIR /${DIR}
+
+COPY src/opi.opam src/Makefile ./
+
+RUN make install-ci
+
+COPY ./src ./
+
+RUN sudo chown -R opam: . && make build
+
+CMD _build/default/exe/main.exe

+ 28 - 0
frameworks/OCaml/opium/src/Makefile

@@ -0,0 +1,28 @@
+project_name = opi
+BINARY = main
+
+.PHONY: install build run gen-opam
+
+install-ci: pin install clean-cache
+
+pin:
+	opam pin add --no-action --yes opium_kernel https://github.com/rgrinberg/opium.git\#a840e4653387fc80529b1c48bc3c66b682eb1426
+	opam pin add --no-action --yes opium        https://github.com/rgrinberg/opium.git\#a840e4653387fc80529b1c48bc3c66b682eb1426
+
+install:
+	opam install --yes --deps-only .
+
+clean-cache:
+	opam clean -a -c -s --logs
+
+gen-opam:
+	dune build @install
+
+build:
+	opam exec -- dune build --profile release exe/$(BINARY).exe
+
+run:
+	./_build/default/exe/$(BINARY).exe
+
+run-debug:
+	dune exec $(project_name) -- --debug

+ 47 - 0
frameworks/OCaml/opium/src/dune-project

@@ -0,0 +1,47 @@
+(lang dune 2.7)
+
+(name opi)
+
+(generate_opam_files true)
+
+(source
+ (github TechEmpower/FrameworkBenchmarks))
+
+(license MIT)
+
+(authors "Robin Bjorklin" "mud")
+
+(maintainers "mud")
+
+(package
+ (name opi)
+ (synopsis "Rudimentary implementation of the Tech Empower Benchmark suite")
+ (depends
+  (dune
+   (>= 2.7.1))
+  (conf-libev
+   (>= 4-11))
+  (lwt
+   (>= 5.3.0))
+  (lwt_ppx
+   (>= 2.0.1))
+  ;; web server
+  (opium
+   (>= 0.18.0))
+  ;; Database interface
+  (caqti
+   (>= 1.2.3))
+  (caqti-lwt
+   (>= 1.2.0))
+  (caqti-driver-postgresql
+   (>= 1.2.4))
+  (ppx_rapper
+   (>= 2.0.0))
+  ;; JSON
+  (yojson
+   (>= 1.7.0))
+  (ppx_deriving_yojson
+   (>= 3.5.3))
+  ;; HTML generation
+  (tyxml
+   (>= 4.4.0))))

+ 4 - 0
frameworks/OCaml/opium/src/exe/dune

@@ -0,0 +1,4 @@
+(executable
+ (public_name opi)
+ (name main)
+ (libraries opi opium caqti caqti-driver-postgresql caqti-lwt tyxml lwt.unix))

+ 41 - 0
frameworks/OCaml/opium/src/exe/main.ml

@@ -0,0 +1,41 @@
+open Opium.Std
+
+let get_count_param req = match (int_of_string (Router.param req "count")) with
+  | x -> x
+  | exception _e -> 1
+
+let main () =
+  let port =
+    match Sys.getenv_opt "PORT" with
+    | Some x -> int_of_string x
+    | None -> 8080
+  in
+  let routes =
+    [
+      "/plaintext", (fun _req -> Opi.Routes.plaintext ());
+      "/json", (fun _req -> Opi.Routes.json ());
+      "/db", (fun _req -> Opi.Routes.db ());
+      "/fortunes", (fun _req -> Opi.Routes.fortunes ());
+      "/queries/", (fun _req -> Opi.Routes.queries 1);
+      "/queries/:count", (fun req -> Opi.Routes.queries (get_count_param req));
+      "/updates/", (fun req -> Opi.Routes.updates 1);
+      "/updates/:count", (fun req -> Opi.Routes.updates (get_count_param req))
+    ]
+  in
+  let add_routes app = List.fold_left (fun app (route,handler) -> (get route handler) app) app routes in
+  let app : Opium.App.t =
+    App.empty
+    |> App.cmd_name "Opium"
+    |> App.port port
+    |> middleware Middleware.content_length
+    |> middleware Tfb_headers.Middleware.m
+    |> add_routes in
+
+  match App.run_command' app with
+  | `Ok (app : unit Lwt.t ) ->
+    let _ = Lwt_io.printf "Running on port 0.0.0.0:%d\n" port in
+    Lwt_main.run app
+  | `Error                  -> exit 1
+  | `Not_running            -> exit 0
+
+let () = main ()

+ 45 - 0
frameworks/OCaml/opium/src/exe/tfb_headers.ml

@@ -0,0 +1,45 @@
+module Time = struct
+  let weekday Unix.{tm_wday;_} = match tm_wday with
+    | 0 -> "Sun"
+    | 1 -> "Mon"
+    | 2 -> "Tue"
+    | 3 -> "Wed"
+    | 4 -> "Thu"
+    | 5 -> "Fri"
+    | 6 -> "Sat"
+    | _ -> failwith "weekday"
+
+  let month Unix.{tm_mon;_} = match tm_mon with
+    | 0 -> "Jan"
+    | 1 -> "Feb"
+    | 2 -> "Mar"
+    | 3 -> "Apr"
+    | 4 -> "May"
+    | 5 -> "Jun"
+    | 6 -> "Jul"
+    | 7 -> "Aug"
+    | 8 -> "Sep"
+    | 9 -> "Oct"
+    | 10 -> "Nov"
+    | 11 -> "Dec"
+    | _ -> failwith "month"
+
+  let gmt tm =
+    Printf.sprintf "%s, %02u %s %04u %02u:%02u:%02u GMT" (weekday tm) tm.tm_mday (month tm) (tm.tm_year + 1900)  tm.tm_hour tm.tm_min tm.tm_sec
+
+  let now () = (gmt (Unix.gmtime (Unix.gettimeofday ())))
+end
+
+module Middleware = struct
+  open Opium.Std
+
+  let m =
+    let open Lwt.Syntax in
+    let filter handler req =
+      let+ res = handler req in
+      let headers = ["Server", "opium"; "Date", Time.now ()] in
+      Response.add_headers_or_replace headers res
+    in
+    Rock.Middleware.create ~name:"Content length" ~filter
+end
+

+ 43 - 0
frameworks/OCaml/opium/src/lib/db.ml

@@ -0,0 +1,43 @@
+module Query = struct
+  type ('res, 'err) query_result = ('res, [> Caqti_error.call_or_retrieve ] as 'err) result Lwt.t
+  type ('res, 'err) query = Caqti_lwt.connection -> ('res, 'err) query_result
+
+  let get_world
+    : id:int -> (Models.World.t, 'err) query =
+    let open Models.World in
+    [%rapper get_one {sql|
+        SELECT @int{id}, @int{randomNumber} FROM World
+        WHERE id = %int{id}
+    |sql} record_out]
+
+  let get_fortunes
+    : unit -> (Models.Fortune.t list, 'err) query =
+    let open Models.Fortune in
+    [%rapper get_many {sql|
+        SELECT @int{id}, @string{message} FROM Fortune
+    |sql} record_out]
+
+  let update_random_number
+    : updated_random_number:int -> id:int -> (unit, 'err) query =
+    [%rapper execute {sql|
+        UPDATE World
+        SET randomNumber = %int{updated_random_number}
+        WHERE id = %int{id}
+    |sql}]
+end
+
+module Get = struct
+  let get_world id : (Models.World.t, string) Lwt_result.t =
+    Pool.query (Query.get_world ~id)
+
+  let get_fortunes () : (Models.Fortune.t list, string) Lwt_result.t =
+    Pool.query (Query.get_fortunes ())
+end
+
+module Update = struct
+  type update_random_numbers_req = { updated_random_number:int; id:int }
+
+  let update_world {updated_random_number; id} =
+    Pool.query (Query.update_random_number ~updated_random_number ~id)
+end
+

+ 6 - 0
frameworks/OCaml/opium/src/lib/dune

@@ -0,0 +1,6 @@
+(library
+ (name opi)
+ (libraries opium caqti caqti-driver-postgresql caqti-lwt tyxml lwt.unix
+   ppx_rapper.runtime)
+ (preprocess
+  (pps ppx_rapper ppx_deriving_yojson)))

+ 10 - 0
frameworks/OCaml/opium/src/lib/models.ml

@@ -0,0 +1,10 @@
+module World = struct
+  type t = { id: int; randomNumber: int }[@@deriving yojson]
+  type list_response = (t list)[@@deriving yojson]
+  type message_response = { message: string }[@@deriving yojson]
+end
+
+module Fortune = struct
+  type t = { id: int; message: string }[@@deriving yojson]
+  let compare a b = String.compare a.message b.message
+end

+ 16 - 0
frameworks/OCaml/opium/src/lib/pool.ml

@@ -0,0 +1,16 @@
+let pool =
+  let connection_url =
+    "postgresql://benchmarkdbuser:benchmarkdbpass@tfb-database:5432/hello_world?connect_timeout=15"
+  in
+  match Caqti_lwt.connect_pool ~max_size:10 (Uri.of_string connection_url) with
+  | Ok pool -> pool
+  | Error err ->
+    Printf.eprintf "%s" (Caqti_error.show err);
+    flush stderr;
+    failwith (Caqti_error.show err)
+
+let query_pool query pool =
+  Caqti_lwt.Pool.use query pool
+  |> Lwt_result.map_err Caqti_error.show
+
+let query q = query_pool q pool

+ 68 - 0
frameworks/OCaml/opium/src/lib/routes.ml

@@ -0,0 +1,68 @@
+let random_int () = Random.int 10000 + 1
+
+let plaintext () =
+  let response = Opium.Std.Response.of_plain_text "Hello, World!" in
+  Lwt.return response
+
+let json () =
+  let json = Models.World.message_response_to_yojson { message="Hello, World!" } in
+  let response = Opium.Std.Response.of_json json in
+  Lwt.return response
+
+let db () =
+  let open Lwt.Syntax in
+  let id = random_int () in
+  let+ result = Db.Get.get_world id in
+  match result with
+  | Error _e -> failwith "failed db"
+  | Ok world -> 
+    let json = Models.World.to_yojson world in
+    Opium.Std.Response.of_json json
+
+let get_worlds count =
+  let count = match count with
+    | x when x < 1 -> 1
+    | x when x > 500 -> 500
+    | x -> x
+  in
+  let query_ids = List.init count (fun _ -> random_int ()) in
+  let open Lwt.Syntax in
+  List.map (fun id ->
+      let+ result = Db.Get.get_world id in
+      match result with
+      | Error _ -> failwith "failed queries"
+      | Ok w -> w
+    ) query_ids
+
+let queries count =
+  let open Lwt.Syntax in
+  let+ worlds = Lwt.all (get_worlds count) in
+  let json = Models.World.list_response_to_yojson worlds in
+  Opium.Std.Response.of_json json
+
+let updates count =
+  let open Lwt.Syntax in
+  let* worlds = Lwt.all (get_worlds count) in
+  let results = worlds |> List.map (fun (world: Models.World.t) ->
+      let updated_random_number = random_int () in
+      let updated_world_req = Db.Update.{updated_random_number;id=world.id} in
+      let+ result = Db.Update.update_world updated_world_req in
+      match result with
+      | Error _ -> failwith "failed queries"
+      | Ok w -> {world with randomNumber=updated_random_number}
+    ) in
+  let+ updated_worlds = Lwt.all results in
+  let json = Models.World.list_response_to_yojson updated_worlds in
+  Opium.Std.Response.of_json json
+
+let fortunes () =
+  let open Lwt.Syntax in
+  let+ result = Db.Get.get_fortunes () in
+  let fortunes = match result with
+    | Error _ -> failwith "failed fortunes"
+    | Ok xs -> 
+      let x = Models.Fortune.{id=0;message= "Additional fortune added at request time."} in
+      x::xs |> List.sort Models.Fortune.compare
+  in
+  Opium.Std.Response.of_html ~indent:false (Views.fortunes_page fortunes)
+

+ 17 - 0
frameworks/OCaml/opium/src/lib/views.ml

@@ -0,0 +1,17 @@
+open Tyxml
+
+let fortunes_table fortunes =
+  let open Html in
+  let table_header = tr [ th [ txt "id" ] ; th [ txt "message" ] ] in
+  let table_row ({id;message}: Models.Fortune.t) = tr [ td [ txt (string_of_int id)] ; td [ txt message ] ] in
+  table
+    (table_header::List.map table_row fortunes)
+
+let fortunes_page fortunes =
+  let open Html in
+  let fortunes_head =
+    head
+      (title (txt "Fortunes"))
+      [] in
+  html fortunes_head (body [ fortunes_table fortunes ])
+

+ 38 - 0
frameworks/OCaml/opium/src/opi.opam

@@ -0,0 +1,38 @@
+# This file is generated by dune, edit dune-project instead
+opam-version: "2.0"
+synopsis: "Rudimentary implementation of the Tech Empower Benchmark suite"
+maintainer: ["mud"]
+authors: ["Robin Bjorklin" "mud"]
+license: "MIT"
+homepage: "https://github.com/TechEmpower/FrameworkBenchmarks"
+bug-reports: "https://github.com/TechEmpower/FrameworkBenchmarks/issues"
+depends: [
+  "dune" {>= "2.7" & >= "2.7.1"}
+  "conf-libev" {>= "4-11"}
+  "lwt" {>= "5.3.0"}
+  "lwt_ppx" {>= "2.0.1"}
+  "opium" {>= "0.18.0"}
+  "caqti" {>= "1.2.3"}
+  "caqti-lwt" {>= "1.2.0"}
+  "caqti-driver-postgresql" {>= "1.2.4"}
+  "ppx_rapper" {>= "2.0.0"}
+  "yojson" {>= "1.7.0"}
+  "ppx_deriving_yojson" {>= "3.5.3"}
+  "tyxml" {>= "4.4.0"}
+  "odoc" {with-doc}
+]
+build: [
+  ["dune" "subst"] {dev}
+  [
+    "dune"
+    "build"
+    "-p"
+    name
+    "-j"
+    jobs
+    "@install"
+    "@runtest" {with-test}
+    "@doc" {with-doc}
+  ]
+]
+dev-repo: "git+https://github.com/TechEmpower/FrameworkBenchmarks.git"

+ 10 - 0
frameworks/OCaml/opium/start-servers.sh

@@ -0,0 +1,10 @@
+#!/bin/sh
+
+CPU_COUNT=$(nproc)
+P=9000
+END=$(($P+$CPU_COUNT))
+
+while [ $P -lt $END ]; do
+  PORT=$P /web/_build/default/exe/main.exe &
+  let P=P+1
+done