git.iko.soy/rowtype-yoga/psfp
on master help sign in
blal
commitbbaf3fbd3533549c1dc6bfeac41f9d1494f6b154 authorMark Eibes <mark.eibes@gmail.com> dateMon 20 Jan 2020, 21:56 (7 years ago) parent8b4e766
16 files changed+557−153
.gitignore+1 client/package-lock.json+36−45 goserver/go.mod−14 playground/package-lock.json+3 playground/src/Main.purs+1−4 server/.vscode/settings.json+1 server/package-lock.json+5 server/package.json+2−1 server/spago.dhall+9−7 server/src/JobQueue.purs+186 server/src/JobQueueSpec.purs+130 server/src/Main.purs+133−75 server/src/Node/OS.js+2 server/src/Node/OS.purs+5 server/test/Main.purs+5−4 shared/Shared/Models/Body.purs+38−3
.gitignore+1
@@ -1,3 +1,4 @@
1+.history/
12client/bower_components/
23client/node_modules/
34client/.pulp-cache/
client/package-lock.json+36−45
@@ -3580,9 +3580,9 @@
35803580 }
35813581 },
35823582 "buffer": {
3583− "version": "4.9.1",
3584− "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
3585− "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
3583+ "version": "4.9.2",
3584+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
3585+ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
35863586 "dev": true,
35873587 "requires": {
35883588 "base64-js": "^1.0.2",
@@ -4206,13 +4206,10 @@
42064206 "dev": true
42074207 },
42084208 "console-browserify": {
4209− "version": "1.1.0",
4210− "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
4211− "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
4212− "dev": true,
4213− "requires": {
4214− "date-now": "^0.1.4"
4215− }
4209+ "version": "1.2.0",
4210+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
4211+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==",
4212+ "dev": true
42164213 },
42174214 "console-control-strings": {
42184215 "version": "1.1.0",
@@ -4612,12 +4609,6 @@
46124609 "whatwg-url": "^7.0.0"
46134610 }
46144611 },
4615− "date-now": {
4616− "version": "0.1.4",
4617− "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
4618− "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
4619− "dev": true
4620− },
46214612 "debug": {
46224613 "version": "3.2.6",
46234614 "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
@@ -4847,9 +4838,9 @@
48474838 "dev": true
48484839 },
48494840 "des.js": {
4850− "version": "1.0.0",
4851− "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
4852− "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
4841+ "version": "1.0.1",
4842+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
4843+ "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==",
48534844 "dev": true,
48544845 "requires": {
48554846 "inherits": "^2.0.1",
@@ -5129,9 +5120,9 @@
51295120 }
51305121 },
51315122 "elliptic": {
5132− "version": "6.5.1",
5133− "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.1.tgz",
5134− "integrity": "sha512-xvJINNLbTeWQjrl6X+7eQCrIy/YPv5XCpKW6kB5mKvtnGILoLDcySuwomfdzt0BMdLNVnuRNTuzKNHj0bva1Cg==",
5123+ "version": "6.5.2",
5124+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
5125+ "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
51355126 "dev": true,
51365127 "requires": {
51375128 "bn.js": "^4.4.0",
@@ -5361,9 +5352,9 @@
53615352 "dev": true
53625353 },
53635354 "events": {
5364− "version": "3.0.0",
5365− "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz",
5366− "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==",
5355+ "version": "3.1.0",
5356+ "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz",
5357+ "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==",
53675358 "dev": true
53685359 },
53695360 "eventsource": {
@@ -11235,9 +11226,9 @@
1123511226 }
1123611227 },
1123711228 "serialize-javascript": {
11238− "version": "1.9.1",
11239− "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz",
11240− "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==",
11229+ "version": "2.1.2",
11230+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
11231+ "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
1124111232 "dev": true
1124211233 },
1124311234 "serve-favicon": {
@@ -11699,9 +11690,9 @@
1169911690 }
1170011691 },
1170111692 "source-map-support": {
11702− "version": "0.5.13",
11703− "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz",
11704− "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==",
11693+ "version": "0.5.16",
11694+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
11695+ "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
1170511696 "dev": true,
1170611697 "requires": {
1170711698 "buffer-from": "^1.0.0",
@@ -12235,9 +12226,9 @@
1223512226 }
1223612227 },
1223712228 "terser": {
12238− "version": "4.3.3",
12239− "resolved": "https://registry.npmjs.org/terser/-/terser-4.3.3.tgz",
12240− "integrity": "sha512-Nzr7dpRjSzMEUS+z2UYQBtzE0LDm5k0Yy8RgLRPy85QUo1TjU5lIOBwzS5/FVAMaVyHZ3WTTU2BuQcMn8KXnNQ==",
12229+ "version": "4.6.3",
12230+ "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz",
12231+ "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==",
1224112232 "dev": true,
1224212233 "requires": {
1224312234 "commander": "^2.20.0",
@@ -12254,16 +12245,16 @@
1225412245 }
1225512246 },
1225612247 "terser-webpack-plugin": {
12257− "version": "1.4.1",
12258− "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.1.tgz",
12259− "integrity": "sha512-ZXmmfiwtCLfz8WKZyYUuuHf3dMYEjg8NrjHMb0JqHVHVOSkzp3cW2/XG1fP3tRhqEqSzMwzzRQGtAPbs4Cncxg==",
12248+ "version": "1.4.3",
12249+ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.3.tgz",
12250+ "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==",
1226012251 "dev": true,
1226112252 "requires": {
1226212253 "cacache": "^12.0.2",
1226312254 "find-cache-dir": "^2.1.0",
1226412255 "is-wsl": "^1.1.0",
1226512256 "schema-utils": "^1.0.0",
12266− "serialize-javascript": "^1.7.0",
12257+ "serialize-javascript": "^2.1.2",
1226712258 "source-map": "^0.6.1",
1226812259 "terser": "^4.1.2",
1226912260 "webpack-sources": "^1.4.0",
@@ -12862,9 +12853,9 @@
1286212853 }
1286312854 },
1286412855 "vm-browserify": {
12865− "version": "1.1.0",
12866− "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.0.tgz",
12867− "integrity": "sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==",
12856+ "version": "1.1.2",
12857+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
12858+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
1286812859 "dev": true
1286912860 },
1287012861 "w3c-hr-time": {
@@ -12929,9 +12920,9 @@
1292912920 "dev": true
1293012921 },
1293112922 "webpack": {
12932− "version": "4.41.0",
12933− "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.0.tgz",
12934− "integrity": "sha512-yNV98U4r7wX1VJAj5kyMsu36T8RPPQntcb5fJLOsMz/pt/WrKC0Vp1bAlqPLkA1LegSwQwf6P+kAbyhRKVQ72g==",
12923+ "version": "4.41.5",
12924+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz",
12925+ "integrity": "sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==",
1293512926 "dev": true,
1293612927 "requires": {
1293712928 "@webassemblyjs/ast": "1.8.5",
@@ -12954,7 +12945,7 @@
1295412945 "node-libs-browser": "^2.2.1",
1295512946 "schema-utils": "^1.0.0",
1295612947 "tapable": "^1.1.3",
12957− "terser-webpack-plugin": "^1.4.1",
12948+ "terser-webpack-plugin": "^1.4.3",
1295812949 "watchpack": "^1.6.0",
1295912950 "webpack-sources": "^1.4.1"
1296012951 }
goserver/go.moddeleted−14
@@ -1,14 +0,0 @@
1−module project.localhost/example
2−
3−replace project.localhost/purescript-native/ffi-loader => ./purescript-native
4−
5−replace project.localhost/purescript-native/output => ./output
6−
7−go 1.13
8−
9−require (
10− github.com/purescript-native/go-ffi v0.0.0-20191015034244-22b13919279c // indirect
11− github.com/purescript-native/go-runtime v0.1.0 // indirect
12− project.localhost/purescript-native/ffi-loader v0.0.0-00010101000000-000000000000 // indirect
13− project.localhost/purescript-native/output v0.0.0-00010101000000-000000000000 // indirect
14−)
playground/package-lock.jsonnew file+3
@@ -0,0 +1,3 @@
1+{
2+ "lockfileVersion": 1
3+}
playground/src/Main.purs+1−4
@@ -1,8 +1,5 @@
11module Main where
2−
32import Prelude
43import Effect.Console
54
6−main = log message
7−
8−message = "Du bist " <> show 7 <> " Jahre alt."
5+main = log "Hi from Insomnia!"
server/.vscode/settings.json+1
@@ -17,4 +17,5 @@
1717 },
1818],
1919"purescript.buildCommand": "spago build --purs-args '--json-errors'",
20+"vscode_custom_css.imports": ["file:///Users/mark/.vscode-oss/extensions/webrender.synthwave-x-fluoromachine-0.0.9/synthwave-x-fluoromachine.css"]
2021}
server/package-lock.json+5
@@ -365,6 +365,11 @@
365365 "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
366366 "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
367367 },
368+ "uuid": {
369+ "version": "3.4.0",
370+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
371+ "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
372+ },
368373 "vary": {
369374 "version": "1.1.2",
370375 "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
server/package.json+2−1
@@ -14,6 +14,7 @@
1414 "license": "ISC",
1515 "dependencies": {
1616 "body-parser": "^1.19.0",
17− "express": "^4.17.1"
17+ "express": "^4.17.1",
18+ "uuid": "^3.4.0"
1819 }
1920}
server/spago.dhall+9−7
@@ -2,10 +2,11 @@
22Welcome to a Spago project!
33You can edit this file as you like.
44-}
5−{ name =
6− "my-project"
5+{ name = "my-project"
76, dependencies =
8− [ "console"
7+ [ "avar"
8+ , "console"
9+ , "debug"
910 , "effect"
1011 , "express"
1112 , "maybe"
@@ -15,10 +16,11 @@ You can edit this file as you like.
1516 , "node-process"
1617 , "psci-support"
1718 , "simple-json"
19+ , "spec"
20+ , "spec-discovery"
1821 , "stringutils"
22+ , "uuid"
1923 ]
20−, packages =
21− ./packages.dhall
22−, sources =
23− [ "src/**/*.purs", "test/**/*.purs" ]
24+, packages = ./packages.dhall
25+, sources = [ "src/**/*.purs", "test/**/*.purs" ]
2426}
server/src/JobQueue.pursnew file+186
@@ -0,0 +1,186 @@
1+module JobQueue where
2+
3+import Prelude
4+
5+import Data.Array (delete, filter, find, length, snoc, zip)
6+import Data.Foldable (for_)
7+import Data.Generic.Rep (class Generic)
8+import Data.Generic.Rep.Show (genericShow)
9+import Data.JSDate (JSDate)
10+import Data.JSDate as JSDate
11+import Data.Maybe (Maybe(..))
12+import Data.Newtype (class Newtype, over, un)
13+import Data.Time.Duration (Milliseconds(..))
14+import Data.Tuple.Nested ((/\))
15+import Data.UUID (UUID, genUUID)
16+import Effect (Effect)
17+import Effect.Aff (Aff, Fiber, delay, error, killFiber, launchAff, launchAff_, parallel, sequential)
18+import Effect.Class (class MonadEffect, liftEffect)
19+import Effect.Ref (Ref)
20+import Effect.Ref as Ref
21+
22+newtype JobId
23+ = JobId UUID
24+
25+derive instance ntJobId :: Newtype JobId _
26+derive instance genericJobId :: Generic JobId _
27+instance showJobId :: Show JobId where
28+ show = genericShow
29+
30+derive newtype instance jobIdEq :: Eq JobId
31+newtype NewJob a
32+ = NewJob (a -> Aff Unit)
33+
34+data PendingJob a
35+ = PendingJob
36+ { id ∷ JobId
37+ , job ∷ a -> Aff Unit
38+ , addedAt ∷ JSDate
39+ }
40+
41+data Job a
42+ = Pending (PendingJob a)
43+ | Running (RunningJob a)
44+
45+getAddedAt ∷ ∀ a. Job a -> JSDate
46+getAddedAt = case _ of
47+ Pending (PendingJob { addedAt }) -> addedAt
48+ Running (RunningJob { addedAt }) -> addedAt
49+
50+instance eqJob :: Eq (Job a) where
51+ eq j1 j2 = getId j1 == getId j2
52+
53+instance ordJob :: Ord (Job a) where
54+ compare j1 j2 = compare (getAddedAt j1) (getAddedAt j2)
55+
56+getId ∷ ∀ a. Job a -> JobId
57+getId = case _ of
58+ Pending (PendingJob { id }) -> id
59+ Running (RunningJob { id }) -> id
60+
61+newtype RunningJob a
62+ = RunningJob
63+ { id ∷ JobId
64+ , addedAt ∷ JSDate
65+ , startedAt ∷ JSDate
66+ , fiber ∷ Fiber Unit
67+ , resource ∷ a
68+ }
69+
70+type QueueParams
71+ = { maxSize ∷ Int
72+ , timeout ∷ Milliseconds
73+ }
74+
75+newtype ResourcePool a
76+ = ResourcePool (Array a)
77+
78+derive instance ntResourcePool :: Newtype (ResourcePool a) _
79+data Queue a
80+ = Queue QueueParams (Ref (ResourcePool a)) (Ref (Array (Job a)))
81+
82+mkQueue ∷ ∀ m a. MonadEffect m => QueueParams -> ResourcePool a -> m (Queue a)
83+mkQueue params pool =
84+ liftEffect do
85+ poolRef <- Ref.new pool
86+ jobsRef <- Ref.new []
87+ pure $ Queue params poolRef jobsRef
88+
89+data EnqueueResult
90+ = Enqueued JobId
91+ | QueueFull
92+
93+derive instance eqEnqueueResult :: Eq EnqueueResult
94+derive instance genericEnqueueResult :: Generic EnqueueResult _
95+instance showEnqueueResult :: Show EnqueueResult where
96+ show = genericShow
97+
98+startJob ∷ ∀ a. Eq a => Queue a -> a -> PendingJob a -> Effect Unit
99+startJob q@(Queue { timeout } _ jobsRef) resource (PendingJob { id, job, addedAt }) = do
100+ startedAt <- JSDate.now
101+ iso <- JSDate.toISOString startedAt
102+ -- log $ "Starting Job: " <> toString (unwrap id) <> " at: " <> iso
103+ let
104+ remove = removeFromQueue id q
105+ fiber <-
106+ launchAff <<< sequential
107+ $ parallel (job resource *> remove)
108+ *> parallel (delay timeout *> remove)
109+ let
110+ running = RunningJob { id, fiber, addedAt, startedAt, resource }
111+ jobsRef
112+ # Ref.modify_ (map \j -> if getId j == id then Running running else j)
113+
114+jobsInQueue ∷ ∀ a. Queue a -> Effect Int
115+jobsInQueue (Queue _ _ jobsRef) = do
116+ jobs <- Ref.read jobsRef
117+ pure $ length jobs
118+
119+runningJobs ∷ ∀ a. Queue a -> Effect (Array (RunningJob a))
120+runningJobs (Queue _ resourceRef jobsRef) = do
121+ jobs <- Ref.read jobsRef
122+ pure do
123+ job <- jobs
124+ case job of
125+ Running rj -> [ rj ]
126+ Pending _ -> []
127+
128+pendingJobs ∷ ∀ a. Queue a -> Effect (Array (PendingJob a))
129+pendingJobs (Queue _ resourceRef jobsRef) = do
130+ jobs <- Ref.read jobsRef
131+ pool <- Ref.read resourceRef
132+ pure do
133+ job <- jobs
134+ case job of
135+ Running _ -> []
136+ Pending pj -> [ pj ]
137+
138+startJobs ∷ ∀ a. Eq a => Queue a -> Effect Unit
139+startJobs q@(Queue { timeout } poolRef jobsRef) = do
140+ pool <- Ref.read poolRef
141+ pending <- pendingJobs q
142+ for_ (pending `zip` un ResourcePool pool) \(job /\ resource) -> do
143+ Ref.modify_ (over ResourcePool $ delete resource) poolRef
144+ startJob q resource job
145+
146+removeStaleJobs ∷ ∀ a. Eq a => Queue a -> Effect Unit
147+removeStaleJobs q@(Queue { timeout } resourceRef jobsRef) = do
148+ running <- runningJobs q
149+ nowMillis <- JSDate.now <#> JSDate.getTime
150+ for_ running
151+ $ \(RunningJob { startedAt, id }) -> do
152+ let
153+ runningSince = (nowMillis - JSDate.getTime startedAt) # Milliseconds
154+
155+ timedOut = runningSince > timeout
156+ when timedOut $ launchAff_ (removeFromQueue id q)
157+
158+removeFromQueue ∷ ∀ a. Eq a => JobId -> Queue a -> Aff Unit
159+removeFromQueue id q@(Queue _ poolRef jobsRef) = do
160+ maybeJob <- Ref.read jobsRef <#> find (\x -> (getId x) == id) # liftEffect
161+ -- remove from queue
162+ jobsRef # Ref.modify_ (filter (\x -> (getId x) /= id)) # liftEffect
163+ case maybeJob of
164+ Just (Running (RunningJob { fiber })) -> killFiber (error "Removed from queue") fiber
165+ _ -> pure unit
166+ -- free resource
167+ case maybeJob of
168+ Just (Running (RunningJob { resource })) -> do
169+ poolRef # Ref.modify_ (over ResourcePool (_ `snoc` resource)) # liftEffect
170+ _ -> pure unit
171+ startJobs q # liftEffect
172+
173+--| Add to the end of the queue. If the queue is full the result will be `false`
174+enqueue ∷ ∀ a. Eq a => NewJob a -> Queue a -> Effect EnqueueResult
175+enqueue (NewJob newJob) q@(Queue { maxSize, timeout } _ jobsRef) = do
176+ id <- genUUID <#> JobId
177+ jobs <- Ref.read jobsRef
178+ if length jobs >= maxSize then
179+ pure QueueFull
180+ else do
181+ addedAt <- JSDate.now
182+ let
183+ new = Pending (PendingJob { id, addedAt, job: newJob })
184+ jobsRef # Ref.modify_ (_ `snoc` new)
185+ startJobs q
186+ pure (Enqueued id)
server/src/JobQueueSpec.pursnew file+130
@@ -0,0 +1,130 @@
1+module JobQueueSpec where
2+
3+import Prelude
4+
5+import Data.Time.Duration (class Duration, Seconds(..), fromDuration)
6+import Effect.Aff (Milliseconds(..), delay)
7+import Effect.Class (liftEffect)
8+import Effect.Class.Console (log)
9+import Effect.Ref (Ref)
10+import Effect.Ref as Ref
11+import JobQueue (EnqueueResult(..), NewJob(..), QueueParams, ResourcePool(..), mkQueue)
12+import JobQueue as Q
13+import Main (Folder(..), execCommand)
14+import Test.Spec (Spec, describe, it)
15+import Test.Spec.Assertions (shouldEqual, shouldNotEqual)
16+
17+params ∷ QueueParams
18+params =
19+ { maxSize: 10
20+ , timeout: (1.0 # Seconds) # fromDuration
21+ }
22+
23+mkJob ∷ String -> Number -> NewJob Unit
24+mkJob msg ms = NewJob \_ -> (delay (ms # Milliseconds) *> log msg)
25+
26+job1 ∷ NewJob Unit
27+job1 = mkJob "Job 1" 10.0
28+
29+writeRefJob ∷ ∀ a d. Show a => Duration d => Ref a -> a -> d -> NewJob Unit
30+writeRefJob ref a d =
31+ NewJob \_ -> do
32+ let
33+ millis ∷ Milliseconds
34+ millis = fromDuration d
35+ delay millis
36+ Ref.write a ref # liftEffect
37+
38+pool :: ResourcePool Unit
39+pool = ResourcePool [unit, unit, unit]
40+
41+spec ∷ Spec Unit
42+spec = do
43+ describe "The Queue" do
44+ it "can't enqueue if maxSize is 0" do
45+ q <- mkQueue (params { maxSize = 0 }) pool
46+ r <- Q.enqueue job1 q # liftEffect
47+ r `shouldEqual` QueueFull
48+ it "can enqueue if maxSize is 1" do
49+ q <- mkQueue (params { maxSize = 1 }) pool
50+ r <- Q.enqueue job1 q # liftEffect
51+ r `shouldNotEqual` QueueFull
52+ it "runsJobs" do
53+ q <- mkQueue (params { maxSize = 1 }) pool
54+ ref <- Ref.new false # liftEffect
55+ r <- Q.enqueue (writeRefJob ref true (0.0 # Seconds)) q # liftEffect
56+ r `shouldNotEqual` QueueFull
57+ delay (5.0 # Milliseconds # fromDuration)
58+ res <- Ref.read ref # liftEffect
59+ res `shouldEqual` true
60+ it "kills slow jobs" do
61+ q <- mkQueue (params { maxSize = 1, timeout = (3.0 # Milliseconds) }) pool
62+ ref <- Ref.new false # liftEffect
63+ r <- Q.enqueue (writeRefJob ref true (4.0 # Milliseconds)) q # liftEffect
64+ r `shouldNotEqual` QueueFull
65+ res <- Ref.read ref # liftEffect
66+ delay (9.0 # Milliseconds)
67+ res `shouldEqual` false
68+ it "runs multiple jobs" do
69+ q <- mkQueue (params { maxSize = 3, timeout = (2.0 # Seconds # fromDuration) }) (ResourcePool [unit])
70+ ref <- Ref.new 0 # liftEffect
71+ let
72+ enq n delay = Q.enqueue (writeRefJob ref n delay) q # liftEffect
73+ r₁ <- enq 1 (0.1 # Seconds)
74+ r₁ `shouldNotEqual` QueueFull
75+ r₂ <- enq 2 (0.2 # Seconds)
76+ r₂ `shouldNotEqual` QueueFull
77+ r₃ <- enq 3 (0.3 # Seconds)
78+ r₃ `shouldNotEqual` QueueFull
79+ Ref.read ref # liftEffect >>= shouldEqual 0 -- ~0ms
80+ delay (0.1 # Seconds # fromDuration)
81+ Ref.read ref # liftEffect >>= shouldEqual 1 -- ~150ms
82+ delay (0.2 # Seconds # fromDuration)
83+ Ref.read ref # liftEffect >>= shouldEqual 2 -- ~250ms
84+ delay (0.3 # Seconds # fromDuration)
85+ Ref.read ref # liftEffect >>= shouldEqual 3 -- ~350ms
86+ it "respects pool size" do
87+ q <- mkQueue (params { maxSize = 3, timeout = (2.0 # Seconds # fromDuration) }) (ResourcePool [unit])
88+ ref <- Ref.new 0 # liftEffect
89+ let
90+ enq n delay = Q.enqueue (writeRefJob ref n delay) q # liftEffect
91+ r₁ <- enq 1 (0.1 # Seconds)
92+ r₁ `shouldNotEqual` QueueFull
93+ r₂ <- enq 2 (0.1 # Seconds)
94+ r₂ `shouldNotEqual` QueueFull
95+ r₃ <- enq 3 (0.1 # Seconds)
96+ r₃ `shouldNotEqual` QueueFull
97+ Ref.read ref # liftEffect >>= shouldEqual 0 -- ~0ms
98+ delay (0.1 # Seconds # fromDuration)
99+ Ref.read ref # liftEffect >>= shouldEqual 1 -- ~150ms
100+ delay (0.2 # Seconds # fromDuration)
101+ Ref.read ref # liftEffect >>= shouldEqual 2 -- ~250ms
102+ delay (0.3 # Seconds # fromDuration)
103+ Ref.read ref # liftEffect >>= shouldEqual 3 -- ~350ms
104+ it "respects pool size (2)" do
105+ q <- mkQueue (params { maxSize = 3, timeout = (2.0 # Seconds # fromDuration) }) (ResourcePool [unit, unit, unit])
106+ ref <- Ref.new 0 # liftEffect
107+ let
108+ enq n delay = Q.enqueue (writeRefJob ref n delay) q # liftEffect
109+ r₁ <- enq 1 (0.1 # Seconds)
110+ r₂ <- enq 2 (0.1 # Seconds)
111+ r₃ <- enq 3 (0.1 # Seconds)
112+ r₁ `shouldNotEqual` QueueFull
113+ r₂ `shouldNotEqual` QueueFull
114+ r₃ `shouldNotEqual` QueueFull
115+ Ref.read ref # liftEffect >>= shouldEqual 0 -- ~0ms
116+ delay (0.15 # Seconds # fromDuration)
117+ Ref.read ref # liftEffect >>= shouldEqual 3 -- ~150ms
118+ it "can kill processes" do
119+ q <- mkQueue (params { maxSize = 1, timeout = (1.0 # Seconds # fromDuration) }) (ResourcePool [unit])
120+ resultRef <- Ref.new false # liftEffect
121+ let
122+ job =
123+ NewJob \_ -> do
124+ res <- execCommand (Folder ".") "sleep 2"
125+ Ref.write true resultRef # liftEffect
126+ r₁ <- Q.enqueue job q # liftEffect
127+ r₁ `shouldNotEqual` QueueFull
128+ delay (1.3 # Seconds # fromDuration)
129+ result <- Ref.read resultRef # liftEffect
130+ result `shouldEqual` false
server/src/Main.purs+133−75
@@ -1,120 +1,178 @@
11module Main where
22
33import Prelude
4−
5−import Data.Array (unsafeIndex)
4+import Data.Array (fromFoldable, head, (..))
65import Data.Either (Either(..))
76import Data.Int (fromString)
87import Data.Maybe (Maybe(..), fromMaybe)
8+import Data.Newtype (class Newtype, un)
99import Data.Posix.Signal (Signal(..))
1010import Data.String.Utils (lines)
11+import Data.Time.Duration (Seconds(..), fromDuration)
1112import Effect (Effect)
12−import Effect.Aff (Aff, effectCanceler, launchAff_, makeAff)
13+import Effect.AVar as AVar
14+import Effect.Aff (Aff, Milliseconds(..), bracket, delay, effectCanceler, launchAff_, makeAff)
1315import Effect.Aff.Class (liftAff)
1416import Effect.Class (class MonadEffect, liftEffect)
15−import Effect.Class.Console (info)
17+import Effect.Class.Console (info, log)
18+import Effect.Exception (throw)
1619import Foreign (unsafeToForeign)
20+import JobQueue (EnqueueResult(..), NewJob(..), Queue, ResourcePool(..))
21+import JobQueue as Q
1722import Middleware.JsonBodyParser (jsonBodyParser)
1823import Node.Buffer (Buffer)
1924import Node.Buffer as Buffer
20−import Node.ChildProcess (ExecResult, defaultExecOptions, exec, kill)
25+import Node.ChildProcess (ExecResult, defaultExecOptions, exec, kill, pid)
2126import Node.Encoding (Encoding(..))
2227import Node.Express.App (App, listenHttp, listenHttps, use, useExternal)
2328import Node.Express.App as E
24−import Node.Express.Handler (HandlerM)
29+import Node.Express.Handler (HandlerM(..), Handler)
2530import Node.Express.Middleware.Static (static)
2631import Node.Express.Request (getBody')
32+import Node.Express.Response (setStatus)
2733import Node.Express.Response as Response
34+import Node.Express.Types (Request, Response)
2835import Node.FS.Aff (readTextFile, writeTextFile)
2936import Node.HTTP (Server)
37+import Node.OS (numCpus)
3038import Node.Process (lookupEnv)
31−import Partial.Unsafe (unsafePartial)
3239import Shared.Json (readAff)
33−import Shared.Models.Body (RunResult)
40+import Shared.Models.Body (RunResult, PursResult)
3441import Shared.Models.Body as Body
3542import Simple.JSON (read_, write)
43+import Simple.JSON as JSON
3644
3745toBody ∷ ∀ r m. MonadEffect m => { stdout ∷ Buffer, stderr ∷ Buffer | r } -> m RunResult
38−toBody result = liftEffect $ ado
39− stdout <- Buffer.toString UTF8 result.stdout
40− stderr <- Buffer.toString UTF8 result.stderr
41− let (code ∷ Maybe Int) = asErrorWithCode result >>= _.code
42− in { code, stdout, stderr } ∷ RunResult
43−
44−type ErrorWithCode = { code ∷ Maybe Int }
46+toBody result =
47+ liftEffect
48+ $ ado
49+ stdout <- Buffer.toString UTF8 result.stdout
50+ stderr <- Buffer.toString UTF8 result.stderr
51+ let
52+ (code ∷ Maybe Int) = asErrorWithCode result >>= _.code
53+ in { code, stdout, stderr } ∷ RunResult
54+
55+type ErrorWithCode
56+ = { code ∷ Maybe Int }
4557
4658asErrorWithCode ∷ ∀ a. a -> Maybe ErrorWithCode
4759asErrorWithCode = read_ <<< unsafeToForeign
4860
49−compileCode ∷ String -> Aff String
50−compileCode code = do
51− saveMainFile code
52− { stderr } <- execCommand "spago build -- --json-errors"
53− errorString <- liftEffect $ Buffer.toString UTF8 stderr
54− pure $ unsafePartial (unsafeIndex (lines errorString) 0)
55−
56−runCode ∷ Aff ExecResult
57−runCode = execCommand "node run.js"
61+compileCode ∷ Folder -> String -> Aff PursResult
62+compileCode folder code = do
63+ saveMainFile folder code
64+ { stderr } <- execCommand folder "spago build --purs-args \"--json-errors\""
65+ strResult <- liftEffect $ Buffer.toString UTF8 stderr
66+ case checkOutput strResult of
67+ Just r -> pure r
68+ Nothing -> liftEffect $ throw $ "No result in " <> strResult
69+ where
70+ checkOutput output =
71+ head do
72+ line <- lines output
73+ let
74+ (parseable :: Maybe PursResult) = JSON.readJSON_ line
75+ fromFoldable parseable
5876
59−playground ∷ String
60−playground = "../playground"
77+runCode ∷ Folder -> Aff ExecResult
78+runCode folder = execCommand folder "node run.js"
6179
62−saveMainFile ∷ String -> Aff Unit
63−saveMainFile code =
64− writeTextFile UTF8 (playground <> "/src/Main.purs") code
80+saveMainFile ∷ Folder -> String -> Aff Unit
81+saveMainFile folder code = writeTextFile UTF8 (un Folder folder <> "/src/Main.purs") code
6582
66−execCommand ∷ String -> Aff ExecResult
67−execCommand command =
83+execCommand ∷ Folder -> String -> Aff ExecResult
84+execCommand folder command =
6885 makeAff \callback -> do
69− let options = defaultExecOptions { cwd = Just playground }
86+ let
87+ options = defaultExecOptions { cwd = Just (un Folder folder) }
7088 childProcess <- exec command options (callback <<< Right)
71− pure $ effectCanceler (kill SIGKILL childProcess)
89+ pure $ effectCanceler ((log $ "Killing " <> show (pid childProcess)) *> kill SIGKILL childProcess)
7290
73−compileHandler ∷ HandlerM Unit
74−compileHandler = do
91+newtype Folder
92+ = Folder String
93+
94+derive instance ntFolder :: Newtype Folder _
95+derive newtype instance eqFolder :: Eq Folder
96+compileHandler ∷ Queue Folder -> HandlerM Unit
97+compileHandler queue = do
7598 body <- getBody'
7699 json <- readAff body # liftAff
77− result <- compileCode (json ∷ Body.CompileRequest).code # liftAff
78− Response.send $ write ({ result } ∷ Body.CompileResult)
79−
80−runHandler ∷ HandlerM Unit
81−runHandler = do
82− result <- liftAff do
83− result <- runCode
84− toBody result
85− Response.send $ write (result ∷ Body.RunResult)
100+ resultVar <- AVar.empty # liftEffect
101+ let
102+ cleanup ∷ Aff Unit
103+ cleanup = AVar.tryPut Nothing resultVar # liftEffect # void
104+
105+ compileJob =
106+ NewJob \folder ->
107+ bracket (pure unit) (const cleanup) \_ -> do
108+ result <- compileCode folder (json ∷ Body.CompileRequest).code
109+ AVar.tryPut (Just result) resultVar # liftEffect # void
110+ queueRes <- Q.enqueue compileJob queue # liftEffect
111+ let
112+ getResult ∷ Handler
113+ getResult = do
114+ maybeResult <- AVar.tryRead resultVar # liftEffect
115+ case maybeResult of
116+ Just (Just result) -> Response.send $ write ({ result } ∷ Body.CompileResult)
117+ Just Nothing -> do
118+ setStatus 500
119+ Response.send $ write { error: "Timeout" }
120+ Nothing -> do
121+ delay (100.0 # Milliseconds) # liftAff
122+ getResult
123+ case queueRes of
124+ Enqueued _ -> getResult
125+ QueueFull -> do
126+ setStatus 500
127+ Response.send $ write { error: "Queue full" }
128+
129+unHandler ∷ ∀ t6. Request -> Response -> Effect Unit -> HandlerM t6 -> Aff t6
130+unHandler req res next (HandlerM h) = h req res next
131+
132+runHandler ∷ Queue Folder -> HandlerM Unit
133+runHandler queue =
134+ HandlerM \req res next -> do
135+ let
136+ handle = unHandler req res next
137+
138+ runJob =
139+ NewJob \folder ->
140+ bracket (pure unit) (const (pure unit)) \_ -> do
141+ result <- runCode folder
142+ resultBody <- toBody result
143+ handle $ Response.send $ write (resultBody ∷ Body.RunResult)
144+ queueRes <- Q.enqueue runJob queue # liftEffect
145+ case queueRes of
146+ Enqueued _ -> pure unit
147+ QueueFull ->
148+ handle do
149+ setStatus 500
150+ Response.send $ write { error: "Queue full" }
86151
87152main ∷ Effect Unit
88−main = launchAff_ do
89− serverSetup makeApp
90−
91−makeApp ∷ App
92−makeApp = do
153+main =
154+ launchAff_ do
155+ cpus <- numCpus # liftEffect
156+ let
157+ foldersToUse = min 2 (max 15 (cpus - 1))
158+
159+ mkFolder = Folder <<< ("../playground" <> _) <<< show
160+
161+ folderPool = ResourcePool $ mkFolder <$> (0 .. foldersToUse)
162+ q <-
163+ Q.mkQueue
164+ { maxSize: 50
165+ , timeout: 60.0 # Seconds # fromDuration
166+ }
167+ folderPool
168+ serverSetup (makeApp q)
169+
170+makeApp ∷ Queue Folder -> App
171+makeApp q = do
93172 use $ static "assets"
94173 useExternal jsonBodyParser
95− E.post "/compile" compileHandler
96− E.post "/run" runHandler
97− -- useOnError (errorHandler log)
98− -- { method: HTTPure.Post, path: ["_compile"], body } -> do
99− -- result <- compileCode body
100− -- HTTPure.ok' (HTTPure.header "Content-Type" "application/json") result
101−
102− -- { method: HTTPure.Post, path: ["_run"] } -> do
103− -- result <- runCode
104− -- response <- toBody result
105− -- okJson response
106−
107− -- { method: HTTPure.Get, path } | elem path [[], [""], ["/"]] -> do
108− -- content <- readTextFile UTF8 "index.html"
109− -- HTTPure.ok' (HTTPure.header "Content-Type" "text/html") content
110−
111− -- { method: HTTPure.Get, path } -> do
112− -- let filePath = intercalate "/" path
113− -- let mimeType = guessMimeType filePath
114− -- content <- readTextFile UTF8 filePath
115− -- HTTPure.ok' (HTTPure.header "Content-Type" mimeType) content
116−
117− -- _ -> HTTPure.notFound
174+ E.post "/compile" (compileHandler q)
175+ E.post "/run" (runHandler q)
118176
119177data CertificatesToUse
120178 = UseLocalCertificates
@@ -128,14 +186,14 @@ parseCertificatesToUse = case _ of
128186serverSetup ∷ App -> Aff Server
129187serverSetup app = do
130188 maybePortString <- lookupEnv "PORT" # liftEffect
131− let port = maybePortString >>= fromString # fromMaybe 14188
189+ let
190+ port = maybePortString >>= fromString # fromMaybe 14188
132191 useLocalCertificates <- fromMaybe DoNotUseLocalCertificates <<< map parseCertificatesToUse <$> lookupEnv "CERTS" # liftEffect
133192 listen <- case useLocalCertificates of
134193 UseLocalCertificates -> do
135194 httpsOptions <- makeHttpsOptions
136195 pure $ listenHttps app (port ∷ Int) httpsOptions
137− DoNotUseLocalCertificates ->
138− pure $ listenHttp app port
196+ DoNotUseLocalCertificates -> pure $ listenHttp app port
139197 liftEffect $ listen \_ -> info $ "psfp server started on port " <> show port
140198 where
141199 makeHttpsOptions = do
server/src/Node/OS.jsnew file+2
@@ -0,0 +1,2 @@
1+var os = require('os')
2+exports.numCpus = function() { return os.cpus().length }
server/src/Node/OS.pursnew file+5
@@ -0,0 +1,5 @@
1+module Node.OS where
2+
3+import Effect (Effect)
4+
5+foreign import numCpus :: Effect Int
server/test/Main.purs+5−4
@@ -3,9 +3,10 @@ module Test.Main where
33import Prelude
44
55import Effect (Effect)
6−import Effect.Class.Console (log)
6+import Effect.Aff (launchAff_)
7+import Test.Spec.Discovery (discover)
8+import Test.Spec.Reporter (consoleReporter)
9+import Test.Spec.Runner (runSpec)
710
811main :: Effect Unit
9−main = do
10− log "🍝"
11− log "You should add some tests."
12+main = launchAff_ $ discover ".*Spec$" >>= runSpec [ consoleReporter ]
shared/Shared/Models/Body.purs+38−3
@@ -2,8 +2,43 @@ module Shared.Models.Body where
22
33import Data.Maybe (Maybe)
44
5−type CompileRequest = { code ∷ String }
5+type CompileRequest
6+ = { code ∷ String }
67
7−type CompileResult = { result ∷ String }
8+type CompileResult
9+ = { result ∷ PursResult }
810
9−type RunResult = { code ∷ Maybe Int, stdout ∷ String, stderr ∷ String }
11+type RunResult
12+ = { code ∷ Maybe Int, stdout ∷ String, stderr ∷ String }
13+
14+type Suggestion
15+ = { replaceRange :: Position , replacement :: String }
16+
17+type Span
18+ = { end :: Array Int
19+ , name :: String
20+ , start :: Array Int
21+ }
22+
23+type Position
24+ = { endColumn :: Int
25+ , endLine :: Int
26+ , startColumn :: Int
27+ , startLine :: Int
28+ }
29+
30+type ErrorOrWarning
31+ = { allSpans :: Array Span
32+ , errorCode :: String
33+ , errorLink :: String
34+ , filename :: String
35+ , message :: String
36+ , moduleName :: Maybe String
37+ , position :: Position
38+ , suggestion :: Maybe Suggestion
39+ }
40+
41+type PursResult
42+ = { errors :: Array ErrorOrWarning
43+ , warnings :: Array ErrorOrWarning
44+ }