woo.ros 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. #|-*- mode:lisp -*-|#
  2. #|
  3. exec ros -Q -- $0 "$@"
  4. |#
  5. ;; Woo is a fast non-blocking HTTP server built on top of
  6. ;; libev. Although Woo is written in Common Lisp, it aims
  7. ;; to be the fastest web server written in any programming
  8. ;; language.
  9. ;; https://github.com/fukamachi/woo
  10. ;; Quicklisp is a library manager for Common Lisp. Use
  11. ;; QuickLisp's quickload function to retrieve external
  12. ;; packages. These packages are automatically curl'd when
  13. ;; the program runs.
  14. ;; Woo - https://github.com/fukamachi/woo
  15. ;; st-json - https://github.com/marijnh/ST-JSON
  16. ;; Serapeum - https://github.com/ruricolist/serapeum
  17. ;; Postmodern - https://github.com/marijnh/Postmodern
  18. ;; QURI - https://github.com/fukamachi/quri
  19. (sb-ext:restrict-compiler-policy 'speed 3)
  20. (sb-ext:restrict-compiler-policy 'safety 0)
  21. (sb-ext:restrict-compiler-policy 'debug 0)
  22. (sb-ext:restrict-compiler-policy 'compilation-speed 0)
  23. (ql:quickload '(postmodern
  24. ;; st-json 10-15% faster than jonathan
  25. ;; according to this benchmark
  26. ;; https://sabracrolleton.github.io/json-review#write-times
  27. st-json
  28. ;; Previously cl-markup was used but Spinneret
  29. ;; is 30% faster and more convenient. CL-WHO also as fast as
  30. ;; Spinneret but requires manual string escaping which is error-prone.
  31. spinneret
  32. quri
  33. uiop
  34. woo
  35. alexandria
  36. serapeum
  37. function-cache
  38. cl+ssl
  39. slynk
  40. slynk/mrepl
  41. slynk-macrostep
  42. slynk-named-readtables
  43. log4cl-extras)
  44. :silent t)
  45. (load (merge-pathnames "helpers/starts-with.lisp"
  46. *load-pathname*))
  47. (load (merge-pathnames "helpers/parse-argv.lisp"
  48. *load-pathname*))
  49. (function-cache:defcached db-host ()
  50. (or (uiop:getenv "DB_HOST")
  51. "tfb-database"))
  52. (function-cache:defcached db-port ()
  53. (parse-integer (or (uiop:getenv "DB_PORT")
  54. "5432")))
  55. (function-cache:defcached db-use-ssl ()
  56. (if (uiop:getenv "DB_USE_SSL")
  57. :full
  58. :no))
  59. (function-cache:defcached db-name ()
  60. (or (uiop:getenv "DB_NAME")
  61. "hello_world"))
  62. (function-cache:defcached db-user ()
  63. (or (uiop:getenv "DB_USER")
  64. "benchmarkdbuser"))
  65. (function-cache:defcached db-pass ()
  66. (or (uiop:getenv "DB_PASS")
  67. "benchmarkdbpass"))
  68. (defmacro with-binary-connection (() &body body)
  69. `(postmodern:with-connection (list (db-name)
  70. (db-user)
  71. (db-pass)
  72. (db-host)
  73. :port (db-port)
  74. :use-binary t
  75. :use-ssl (db-use-ssl)
  76. :pooled-p t)
  77. ,@body))
  78. (declaim (ftype (function () list)
  79. plaintext)
  80. (inline plaintext))
  81. (defun plaintext ()
  82. "Plaintext handler."
  83. '(200
  84. (:content-type "text/plain"
  85. :server "Woo")
  86. ("Hello, World!")))
  87. (declaim (ftype (function () list)
  88. json)
  89. (inline json))
  90. (defun json ()
  91. "JSON handler using Jonathan to encode JSON"
  92. `(200
  93. (:content-type "application/json"
  94. :server "Woo")
  95. (,(st-json:write-json-to-string (serapeum:dict "message"
  96. "Hello, World!")))))
  97. (postmodern:defprepared get-a-random-record-query
  98. (:select 'randomnumber
  99. :from 'world
  100. :where (:= 'id '$1))
  101. :single)
  102. (declaim (ftype (function (fixnum) (values))
  103. get-a-random-record)
  104. (inline get-a-random-record))
  105. (defun get-a-random-record (id)
  106. (declare (fixnum id))
  107. (let ((id (min id 10000)))
  108. (let ((number (get-a-random-record-query id)))
  109. (declare (type fixnum number))
  110. (serapeum:dict "id" id
  111. "randomNumber" number))))
  112. (declaim (ftype (function () list)
  113. db)
  114. (inline db))
  115. (defun db ()
  116. "DB handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
  117. (with-binary-connection ()
  118. (let ((id (+ 1 (random 10000))))
  119. `(200
  120. (:content-type "application/json"
  121. :server "Woo")
  122. (,(st-json:write-json-to-string (get-a-random-record id)))))))
  123. (declaim (ftype (function (fixnum) fixnum)
  124. ensure-integer-is-between-one-and-five-hundreds)
  125. (inline ensure-integer-is-between-one-and-five-hundreds))
  126. (defun ensure-integer-is-between-one-and-five-hundreds (n)
  127. (max (min n 500)
  128. 1))
  129. (declaim (ftype (function (list) fixnum)
  130. extract-number-of-records-to-fetch)
  131. (inline extract-number-of-records-to-fetch))
  132. (defun extract-number-of-records-to-fetch (env)
  133. (let ((n (handler-case
  134. (parse-integer (cdr (assoc "queries" (quri:url-decode-params (getf env :query-string)) :test #'equal)))
  135. (error (c) (values 1 c)))))
  136. (ensure-integer-is-between-one-and-five-hundreds n)))
  137. (declaim (ftype (function (fixnum) list)
  138. get-some-random-integers-between-one-and-ten-thousand)
  139. (inline get-some-random-integers-between-one-and-ten-thousand))
  140. (defun get-some-random-integers-between-one-and-ten-thousand (n)
  141. (declare (fixnum n))
  142. (loop :repeat n
  143. :collect (+ 1 (random 10000))))
  144. (declaim (ftype (function (fixnum) list)
  145. get-some-random-records)
  146. (inline get-some-random-records))
  147. (defun get-some-random-records (n)
  148. (loop repeat n
  149. for id fixnum = (1+ (random 10000))
  150. collect (get-a-random-record id)))
  151. (declaim (ftype (function (list) list)
  152. queries)
  153. (inline queries))
  154. (defun queries (env)
  155. "QUERIES handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
  156. (with-binary-connection ()
  157. `(200
  158. (:content-type "application/json"
  159. :server "Woo")
  160. (,(st-json:write-json-to-string
  161. (get-some-random-records
  162. (extract-number-of-records-to-fetch env)))))))
  163. (postmodern:defprepared get-all-fortunes
  164. (:select 'id 'message
  165. :from 'fortune)
  166. :rows)
  167. (declaim (ftype (function () list)
  168. get-all-fortunes-plus-one)
  169. (inline get-all-fortunes-plus-one))
  170. (defun get-all-fortunes-plus-one ()
  171. (let* ((records (get-all-fortunes))
  172. (records-p-one
  173. (append records '((0 "Additional fortune added at request time.")))))
  174. (sort (copy-list records-p-one) #'string-lessp :key #'second)))
  175. (declaim (ftype (function () list)
  176. fortunes)
  177. (inline fortunes))
  178. (defun fortunes ()
  179. "FORTUNES handler using Spinneret to generate HTML and Postmodern to access PostgreSQL."
  180. (let ((*print-pretty* nil)
  181. ;; Without this setting Spinneret does not close tags when it is possible
  182. ;; and benchmark's validator fails.
  183. (spinneret:*html-style* :tree))
  184. (with-binary-connection ()
  185. `(200
  186. (:content-type "text/html; charset=UTF-8"
  187. :server "Woo")
  188. (,(spinneret:with-html-string
  189. (:doctype)
  190. (:html
  191. ;; Here I have to use :tag,
  192. ;; because otherwise spinneret inserts
  193. ;; <meta charset="UTF-8" /> and benchmark's verification fails.
  194. (:tag :name "head"
  195. (:title "Fortunes"))
  196. (:body
  197. (:table
  198. (:tr
  199. (:th "id")
  200. (:th "message"))
  201. (loop for fortune-row in (get-all-fortunes-plus-one)
  202. do (:tr
  203. (:td (format nil "~d" (first fortune-row)))
  204. (:td (second fortune-row)))))))))))))
  205. (defun make-batch-update-query (n)
  206. (declare (type fixnum n))
  207. (format nil
  208. "UPDATE world AS ori SET randomnumber = new.randomnumber::integer FROM (VALUES ~{~A~^, ~}) AS new (id, randomnumber) WHERE ori.id = new.id::integer"
  209. (loop for i fixnum below n
  210. for arg1 fixnum = (+ (* i 2)
  211. 1)
  212. for arg2 fixnum = (+ (* i 2)
  213. 2)
  214. collect (format nil "($~A,$~A)" arg1 arg2))))
  215. (defparameter *batch-updaters* (make-hash-table))
  216. (defmacro define-batch-updater (n)
  217. (let ((name (intern (format nil "UPDATE-BATCH-~A" n)))
  218. (query (make-batch-update-query n)))
  219. `(progn
  220. (postmodern:defprepared ,name
  221. (:raw ,query)
  222. :rows)
  223. (setf (gethash ,n *batch-updaters*)
  224. #',name))))
  225. (defmacro define-batch-updaters (n)
  226. (loop for i fixnum from 1 to n
  227. collect `(define-batch-updater ,i) into forms
  228. finally (return `(progn ,@forms))))
  229. ;; Here we are defining a number of functions
  230. ;; which use prepared statements to update
  231. ;; a given number of records.
  232. ;;
  233. ;; Each function does something like this:
  234. ;;
  235. ;; UPDATE world AS ori SET randomnumber = new.randomnumber FROM
  236. ;; (VALUES (($1, $2), ($3, $4), ($5, $6), ($7, $8), ($9, $10)) AS new (id, randomnumber) WHERE ori.id = new.id
  237. ;;
  238. ;; Previous version of the update function formatted SQL query
  239. ;; using FORMAT function. In real world application this could lead to SQL injection.
  240. (define-batch-updaters 500)
  241. (defun auto-batch-update (data)
  242. (declare (type list data))
  243. (let* ((batch-length (/ (length data)
  244. 2))
  245. (func (gethash batch-length
  246. *batch-updaters*)))
  247. (declare (type (or null function) func))
  248. (cond
  249. (func
  250. (apply func data))
  251. (t
  252. (error "No prepared function for batch of length ~A"
  253. batch-length)))))
  254. (declaim (ftype (function (fixnum) list)
  255. get-and-update-some-random-records-batch)
  256. (inline get-and-update-some-random-records-batch))
  257. (defun get-and-update-some-random-records-batch (n)
  258. "Flexible batch updater. This function will be called with n from 0 upto 500."
  259. (let* ((random-records (get-some-random-records n))
  260. (random-numbers (get-some-random-integers-between-one-and-ten-thousand n)))
  261. (loop with batch-size fixnum = 0
  262. with args = nil
  263. for iteration fixnum upfrom 0
  264. for row in random-records
  265. for new-random-number in random-numbers
  266. for record-id = (gethash "id" row)
  267. do (setf args (list*
  268. record-id
  269. new-random-number
  270. args))
  271. (setf batch-size
  272. (1+ batch-size))
  273. ;; Here we keep old hash but update it with a new
  274. ;; value to make lisp consing less:
  275. (setf (gethash "randomNumber" row)
  276. new-random-number)
  277. collect row into results
  278. when (= batch-size
  279. 500)
  280. ;; Sending update to the database.
  281. ;; This branch works if n > 500
  282. do (apply #'update-batch-500 args)
  283. (setf args nil
  284. batch-size 0)
  285. finally (when args
  286. ;; If we have some more data to update
  287. ;; then will send them to the database too.
  288. (auto-batch-update args))
  289. (return results))))
  290. (declaim (ftype (function (list) list)
  291. updates)
  292. (inline updates))
  293. (defun updates (env)
  294. "UPDATES handler using Jonathan to encode JSON and Postmodern to access PostgreSQL"
  295. (with-binary-connection ()
  296. `(200
  297. (:content-type "application/json"
  298. :server "Woo")
  299. (,(st-json:write-json-to-string (get-and-update-some-random-records-batch
  300. (extract-number-of-records-to-fetch env)))))))
  301. (defparameter *args* nil)
  302. (declaim (ftype (function () list)
  303. server-info)
  304. (inline server-info))
  305. (defun server-info ()
  306. "Shows information about lisp implementation and version"
  307. `(200
  308. (:content-type "text/plain"
  309. :server "Woo")
  310. (,(format nil "Running on ~A ~A~%Started with: ~A~%"
  311. (lisp-implementation-type)
  312. (lisp-implementation-version)
  313. *args*))))
  314. (declaim (ftype (function (list) list)
  315. handler)
  316. (inline handler))
  317. (defun handler (env)
  318. "Router"
  319. (log4cl-extras/error:with-log-unhandled ()
  320. (let ((path (getf env :path-info)))
  321. (cond ((starts-with path "/plaintext") (plaintext))
  322. ((starts-with path "/json" ) (json))
  323. ((starts-with path "/db" ) (db))
  324. ((starts-with path "/queries" ) (queries env))
  325. ((starts-with path "/fortunes" ) (fortunes))
  326. ((starts-with path "/updates" ) (updates env))
  327. (t
  328. (server-info))))))
  329. (defvar slynk:*use-dedicated-output-stream*)
  330. (defun main (&rest argv)
  331. "Create and start the server, applying argv to the env"
  332. (let ((args (parse-argv argv))
  333. (debug-mode (uiop:getenv "DEBUG")))
  334. ;; Initialize the global random state by "some means" (e.g. current time)
  335. (setf *random-state* (make-random-state t))
  336. (setf *args* args)
  337. (setf (getf args :worker-num)
  338. ;; empirically I found that performance is the best when
  339. ;; we have 4 workers per core.
  340. (* (getf args :cpu 1)
  341. 4))
  342. (alexandria:remove-from-plistf args :cpu)
  343. (format t "Starting with args: ~S~%"
  344. args)
  345. (when debug-mode
  346. (setf slynk:*use-dedicated-output-stream* nil)
  347. (slynk:create-server :port 4005
  348. :interface "0.0.0.0"
  349. :dont-close t))
  350. (when (db-use-ssl)
  351. (let ((postgres-certs-file
  352. (probe-file "~/.postgresql/root.crt")))
  353. (when postgres-certs-file
  354. (cl+ssl:ssl-load-global-verify-locations postgres-certs-file))))
  355. (log4cl-extras/config:setup
  356. (list :level (if debug-mode
  357. :debug
  358. :error)
  359. :appenders '((this-console :layout :plain))))
  360. (apply #'woo:run
  361. #'handler
  362. :debug nil
  363. args)))