123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- #|-*- mode:lisp -*-|#
- #|
- exec ros -Q -- $0 "$@"
- |#
- ;; Woo is a fast non-blocking HTTP server built on top of
- ;; libev. Although Woo is written in Common Lisp, it aims
- ;; to be the fastest web server written in any programming
- ;; language.
- ;; https://github.com/fukamachi/woo
- ;; Quicklisp is a library manager for Common Lisp. Use
- ;; QuickLisp's quickload function to retrieve external
- ;; packages. These packages are automatically curl'd when
- ;; the program runs.
- ;; Woo - https://github.com/fukamachi/woo
- ;; st-json - https://github.com/marijnh/ST-JSON
- ;; Serapeum - https://github.com/ruricolist/serapeum
- ;; Postmodern - https://github.com/marijnh/Postmodern
- ;; QURI - https://github.com/fukamachi/quri
- (sb-ext:restrict-compiler-policy 'speed 3)
- (sb-ext:restrict-compiler-policy 'safety 0)
- (sb-ext:restrict-compiler-policy 'debug 0)
- (sb-ext:restrict-compiler-policy 'compilation-speed 0)
- (ql:quickload '(postmodern
- ;; st-json 10-15% faster than jonathan
- ;; according to this benchmark
- ;; https://sabracrolleton.github.io/json-review#write-times
- st-json
- ;; Previously cl-markup was used but Spinneret
- ;; is 30% faster and more convenient. CL-WHO also as fast as
- ;; Spinneret but requires manual string escaping which is error-prone.
- spinneret
- quri
- uiop
- woo
- alexandria
- serapeum
- function-cache
- cl+ssl
- slynk
- slynk/mrepl
- slynk-macrostep
- slynk-named-readtables
- log4cl-extras)
- :silent t)
- (load (merge-pathnames "helpers/starts-with.lisp"
- *load-pathname*))
- (load (merge-pathnames "helpers/parse-argv.lisp"
- *load-pathname*))
- (function-cache:defcached db-host ()
- (or (uiop:getenv "DB_HOST")
- "tfb-database"))
- (function-cache:defcached db-port ()
- (parse-integer (or (uiop:getenv "DB_PORT")
- "5432")))
- (function-cache:defcached db-use-ssl ()
- (if (uiop:getenv "DB_USE_SSL")
- :full
- :no))
- (function-cache:defcached db-name ()
- (or (uiop:getenv "DB_NAME")
- "hello_world"))
- (function-cache:defcached db-user ()
- (or (uiop:getenv "DB_USER")
- "benchmarkdbuser"))
- (function-cache:defcached db-pass ()
- (or (uiop:getenv "DB_PASS")
- "benchmarkdbpass"))
- (defmacro with-binary-connection (() &body body)
- `(postmodern:with-connection (list (db-name)
- (db-user)
- (db-pass)
- (db-host)
- :port (db-port)
- :use-binary t
- :use-ssl (db-use-ssl)
- :pooled-p t)
- ,@body))
- (declaim (ftype (function () list)
- plaintext)
- (inline plaintext))
- (defun plaintext ()
- "Plaintext handler."
- '(200
- (:content-type "text/plain"
- :server "Woo")
- ("Hello, World!")))
- (declaim (ftype (function () list)
- json)
- (inline json))
- (defun json ()
- "JSON handler using Jonathan to encode JSON"
- `(200
- (:content-type "application/json"
- :server "Woo")
- (,(st-json:write-json-to-string (serapeum:dict "message"
- "Hello, World!")))))
- (postmodern:defprepared get-a-random-record-query
- (:select 'randomnumber
- :from 'world
- :where (:= 'id '$1))
- :single)
- (declaim (ftype (function (fixnum) (values))
- get-a-random-record)
- (inline get-a-random-record))
- (defun get-a-random-record (id)
- (declare (fixnum id))
-
- (let ((id (min id 10000)))
- (let ((number (get-a-random-record-query id)))
- (declare (type fixnum number))
- (serapeum:dict "id" id
- "randomNumber" number))))
- (declaim (ftype (function () list)
- db)
- (inline db))
- (defun db ()
- "DB handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
- (with-binary-connection ()
- (let ((id (+ 1 (random 10000))))
- `(200
- (:content-type "application/json"
- :server "Woo")
- (,(st-json:write-json-to-string (get-a-random-record id)))))))
- (declaim (ftype (function (fixnum) fixnum)
- ensure-integer-is-between-one-and-five-hundreds)
- (inline ensure-integer-is-between-one-and-five-hundreds))
- (defun ensure-integer-is-between-one-and-five-hundreds (n)
- (max (min n 500)
- 1))
- (declaim (ftype (function (list) fixnum)
- extract-number-of-records-to-fetch)
- (inline extract-number-of-records-to-fetch))
- (defun extract-number-of-records-to-fetch (env)
- (let ((n (handler-case
- (parse-integer (cdr (assoc "queries" (quri:url-decode-params (getf env :query-string)) :test #'equal)))
- (error (c) (values 1 c)))))
- (ensure-integer-is-between-one-and-five-hundreds n)))
- (declaim (ftype (function (fixnum) list)
- get-some-random-integers-between-one-and-ten-thousand)
- (inline get-some-random-integers-between-one-and-ten-thousand))
- (defun get-some-random-integers-between-one-and-ten-thousand (n)
- (declare (fixnum n))
- (loop :repeat n
- :collect (+ 1 (random 10000))))
- (declaim (ftype (function (fixnum) list)
- get-some-random-records)
- (inline get-some-random-records))
- (defun get-some-random-records (n)
- (loop repeat n
- for id fixnum = (1+ (random 10000))
- collect (get-a-random-record id)))
- (declaim (ftype (function (list) list)
- queries)
- (inline queries))
- (defun queries (env)
- "QUERIES handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
- (with-binary-connection ()
- `(200
- (:content-type "application/json"
- :server "Woo")
- (,(st-json:write-json-to-string
- (get-some-random-records
- (extract-number-of-records-to-fetch env)))))))
- (postmodern:defprepared get-all-fortunes
- (:select 'id 'message
- :from 'fortune)
- :rows)
- (declaim (ftype (function () list)
- get-all-fortunes-plus-one)
- (inline get-all-fortunes-plus-one))
- (defun get-all-fortunes-plus-one ()
- (let* ((records (get-all-fortunes))
- (records-p-one
- (append records '((0 "Additional fortune added at request time.")))))
- (sort (copy-list records-p-one) #'string-lessp :key #'second)))
- (declaim (ftype (function () list)
- fortunes)
- (inline fortunes))
- (defun fortunes ()
- "FORTUNES handler using Spinneret to generate HTML and Postmodern to access PostgreSQL."
- (let ((*print-pretty* nil)
- ;; Without this setting Spinneret does not close tags when it is possible
- ;; and benchmark's validator fails.
- (spinneret:*html-style* :tree))
- (with-binary-connection ()
- `(200
- (:content-type "text/html; charset=UTF-8"
- :server "Woo")
- (,(spinneret:with-html-string
- (:doctype)
- (:html
- ;; Here I have to use :tag,
- ;; because otherwise spinneret inserts
- ;; <meta charset="UTF-8" /> and benchmark's verification fails.
- (:tag :name "head"
- (:title "Fortunes"))
- (:body
- (:table
- (:tr
- (:th "id")
- (:th "message"))
- (loop for fortune-row in (get-all-fortunes-plus-one)
- do (:tr
- (:td (format nil "~d" (first fortune-row)))
- (:td (second fortune-row)))))))))))))
- (defun make-batch-update-query (n)
- (declare (type fixnum n))
- (format nil
- "UPDATE world AS ori SET randomnumber = new.randomnumber::integer FROM (VALUES ~{~A~^, ~}) AS new (id, randomnumber) WHERE ori.id = new.id::integer"
- (loop for i fixnum below n
- for arg1 fixnum = (+ (* i 2)
- 1)
- for arg2 fixnum = (+ (* i 2)
- 2)
- collect (format nil "($~A,$~A)" arg1 arg2))))
- (defparameter *batch-updaters* (make-hash-table))
- (defmacro define-batch-updater (n)
- (let ((name (intern (format nil "UPDATE-BATCH-~A" n)))
- (query (make-batch-update-query n)))
- `(progn
- (postmodern:defprepared ,name
- (:raw ,query)
- :rows)
- (setf (gethash ,n *batch-updaters*)
- #',name))))
- (defmacro define-batch-updaters (n)
- (loop for i fixnum from 1 to n
- collect `(define-batch-updater ,i) into forms
- finally (return `(progn ,@forms))))
- ;; Here we are defining a number of functions
- ;; which use prepared statements to update
- ;; a given number of records.
- ;;
- ;; Each function does something like this:
- ;;
- ;; UPDATE world AS ori SET randomnumber = new.randomnumber FROM
- ;; (VALUES (($1, $2), ($3, $4), ($5, $6), ($7, $8), ($9, $10)) AS new (id, randomnumber) WHERE ori.id = new.id
- ;;
- ;; Previous version of the update function formatted SQL query
- ;; using FORMAT function. In real world application this could lead to SQL injection.
- (define-batch-updaters 500)
- (defun auto-batch-update (data)
- (declare (type list data))
- (let* ((batch-length (/ (length data)
- 2))
- (func (gethash batch-length
- *batch-updaters*)))
- (declare (type (or null function) func))
- (cond
- (func
- (apply func data))
- (t
- (error "No prepared function for batch of length ~A"
- batch-length)))))
- (declaim (ftype (function (fixnum) list)
- get-and-update-some-random-records-batch)
- (inline get-and-update-some-random-records-batch))
- (defun get-and-update-some-random-records-batch (n)
- "Flexible batch updater. This function will be called with n from 0 upto 500."
- (let* ((random-records (get-some-random-records n))
- (random-numbers (get-some-random-integers-between-one-and-ten-thousand n)))
- (loop with batch-size fixnum = 0
- with args = nil
- for iteration fixnum upfrom 0
- for row in random-records
- for new-random-number in random-numbers
- for record-id = (gethash "id" row)
-
- do (setf args (list*
- record-id
- new-random-number
- args))
- (setf batch-size
- (1+ batch-size))
- ;; Here we keep old hash but update it with a new
- ;; value to make lisp consing less:
- (setf (gethash "randomNumber" row)
- new-random-number)
- collect row into results
- when (= batch-size
- 500)
- ;; Sending update to the database.
- ;; This branch works if n > 500
- do (apply #'update-batch-500 args)
- (setf args nil
- batch-size 0)
- finally (when args
- ;; If we have some more data to update
- ;; then will send them to the database too.
- (auto-batch-update args))
- (return results))))
- (declaim (ftype (function (list) list)
- updates)
- (inline updates))
- (defun updates (env)
- "UPDATES handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
- (with-binary-connection ()
- `(200
- (:content-type "application/json"
- :server "Woo")
- (,(st-json:write-json-to-string (get-and-update-some-random-records-batch
- (extract-number-of-records-to-fetch env)))))))
- (defparameter *args* nil)
- (declaim (ftype (function () list)
- server-info)
- (inline server-info))
- (defun server-info ()
- "Shows information about lisp implementation and version"
- `(200
- (:content-type "text/plain"
- :server "Woo")
- (,(format nil "Running on ~A ~A~%Started with: ~A~%"
- (lisp-implementation-type)
- (lisp-implementation-version)
- *args*))))
- (declaim (ftype (function (list) list)
- handler)
- (inline handler))
- (defun handler (env)
- "Router"
- (log4cl-extras/error:with-log-unhandled ()
- (let ((path (getf env :path-info)))
- (cond ((starts-with path "/plaintext") (plaintext))
- ((starts-with path "/json" ) (json))
- ((starts-with path "/db" ) (db))
- ((starts-with path "/queries" ) (queries env))
- ((starts-with path "/fortunes" ) (fortunes))
- ((starts-with path "/updates" ) (updates env))
- (t
- (server-info))))))
- (defvar slynk:*use-dedicated-output-stream*)
- (defun main (&rest argv)
- "Create and start the server, applying argv to the env"
- (let ((args (parse-argv argv))
- (debug-mode (uiop:getenv "DEBUG")))
- ;; Initialize the global random state by "some means" (e.g. current time)
- (setf *random-state* (make-random-state t))
-
- (setf *args* args)
-
- (setf (getf args :worker-num)
- ;; empirically I found that performance is the best when
- ;; we have 4 workers per core.
- (* (getf args :cpu 1)
- 4))
- (alexandria:remove-from-plistf args :cpu)
-
- (format t "Starting with args: ~S~%"
- args)
-
- (when debug-mode
- (setf slynk:*use-dedicated-output-stream* nil)
- (slynk:create-server :port 4005
- :interface "0.0.0.0"
- :dont-close t))
-
- (when (db-use-ssl)
- (let ((postgres-certs-file
- (probe-file "~/.postgresql/root.crt")))
- (when postgres-certs-file
- (cl+ssl:ssl-load-global-verify-locations postgres-certs-file))))
- (log4cl-extras/config:setup
- (list :level (if debug-mode
- :debug
- :error)
- :appenders '((this-console :layout :plain))))
-
- (apply #'woo:run
- #'handler
- :debug nil
- args)))
|