streaming-v2.tri (9609B)
1 !import base as B 2 !import native_math as N 3 !import io_v2 as IO 4 !import io_v2_http as Transport 5 !import io_v2_artifacts as Stream 6 !import http as HTTP 7 !import handlers as H 8 !import handler_artifacts as Artifacts 9 !import handler_log as Log 10 !import arboricx_handlers as Registry 11 !import arboricx_server as Store 12 !import conversions as Convert 13 !export pump 14 !export connection 15 !export serve 16 !export serveCount 17 18 pump _ 0 _ = H.pure (B.ok t t) 19 pump client remaining initial = let 20 limit = N.minimum remaining 4096 21 consume (unused, (reversed, rest)) = case reversed of 22 [] = H.bind (H.liftIO (IO.readSome client limit)) (reply : case reply of 23 ((t t), (("data", chunk), t)) = pump client remaining chunk 24 _ = H.pure (B.err "Unexpected end of request body" t)) 25 _ = H.bind (Stream.write (B.reverse reversed)) (reply : case reply of 26 ((t t), (t, t)) = pump client (N.subtract remaining (N.subtract limit unused)) rest 27 _ = H.pure reply) 28 consume _ = H.fail "invalid-response" "artifact-stream" "buffer" "Invalid body state" 29 in consume (HTTP.takeBodyBytes initial limit []) 30 31 logged sink status response = H.bind 32 (Log.withSink sink (Log.withContext [["method" "POST"] ["target" "/_arboricx/bundle"]] (Log.info status))) 33 (reply : case reply of 34 ((t t), (t, t)) = H.pure response 35 _ = H.pure (HTTP.errorResponse 500 "Internal Server Error\n")) 36 upload root sink client count initial = H.bind 37 (Artifacts.publishStreamV2 root count (H.label "streaming-upload" (pump client count initial))) 38 (reply : case reply of 39 ((t t), (digest, t)) = logged sink 201 (HTTP.createdResponse digest) 40 (t, (_, t)) = logged sink 400 (HTTP.badRequestResponse "Upload failed\n") 41 _ = H.fail "invalid-response" "artifact-stream" "publish" "Invalid upload Result") 42 -- Supervised request interpretation retains the socket in its connection owner; 43 -- borrowing does not extend lifetime. Join before responding, so cleanup failure 44 -- cannot falsely acknowledge success. Cancellation is never rendered as HTTP 500. 45 response root sink client count initial = IO.scope (_ : IO.require 46 (IO.spawn 1 1 [] (_ : H.runIOV2 (upload root sink client count initial))) (spawned : case spawned of 47 [_ ticket] = IO.require (IO.join ticket) (outcome : case outcome of 48 ["completed" value] = IO.pure value 49 ["failed" _ _] = IO.pure (HTTP.errorResponse 500 "Internal Server Error\n") 50 ["cancelled" reason _] = IO.cancelled reason 51 _ = IO.fail "Invalid upload task outcome") 52 _ = IO.fail "Invalid upload spawn reply")) 53 boundedResponse root limit sink client count initial = case N.lte count limit of 54 (t t) = IO.bind (response root sink client count initial) (Transport.respond client "POST") 55 _ = Transport.respond client "POST" (HTTP.errorResponse 413 "Request body too large\n") 56 framed root limit sink client fields initial = case HTTP.contentLength fields of 57 ((t t), (t, _)) = boundedResponse root limit sink client 0 initial 58 ((t t), ((t count), _)) = boundedResponse root limit sink client count initial 59 (t, (status, message)) = Transport.respond client "POST" (HTTP.errorResponse status message) 60 _ = Transport.respond client "POST" (HTTP.badRequestResponse "Bad Request\n") 61 headers root limit sink client version raw = case HTTP.parseHeaders raw of 62 ((t t), (fields, initial)) = case HTTP.validateRequest "POST" "/_arboricx/bundle" version fields of 63 ((t t), _) = framed root limit sink client fields initial 64 (t, (status, message)) = Transport.respond client "POST" (HTTP.errorResponse status message) 65 _ = Transport.respond client "POST" (HTTP.badRequestResponse "Bad Request\n") 66 _ = Transport.respond client "" (HTTP.badRequestResponse "Bad Request\n") 67 -- Log before committing headers. Supervision contains logger failures without 68 -- letting a post-header transfer failure generate a second HTTP response. 69 downloadLog sink method target status = IO.scope (_ : IO.require 70 (IO.spawn 1 1 [] (_ : H.runIOV2 71 (Log.withSink sink (Log.withContext [["method" method] ["target" target]] (Log.info status))))) 72 (spawned : case spawned of 73 [_ ticket] = IO.require (IO.join ticket) (outcome : case outcome of 74 ["completed" ((t t), (t, t))] = IO.pure (t t) 75 ["cancelled" reason _] = IO.cancelled reason 76 _ = IO.pure t) 77 _ = IO.fail "Invalid download logger task")) 78 downloadError sink client method target status message = IO.bind 79 (downloadLog sink method target status) (logged : case logged of 80 (t t) = Transport.respond client method (HTTP.errorResponse status message) 81 _ = Transport.respond client method (HTTP.errorResponse 500 "Internal Server Error\n")) 82 prepareDownload owner root limit digest = do IO.onOk 83 reader <- IO.openReader owner (Store.bundleObjectPath root digest) 84 snapshot <- IO.snapshotBytes reader (Store.pathJoin root "tmp") limit digest 85 IO.close reader 86 IO.pure (B.ok snapshot t) 87 sendDownload sink client method target [reader size] = IO.bind 88 (downloadLog sink method target 200) (logged : case logged of 89 (t t) = do IO.bind 90 Transport.respond client method (HTTP.buildResponse 200 91 [(B.pair "Content-Type" Store.bundleContentType) 92 (B.pair "Content-Length" (Convert.showNumber size)) 93 (B.pair "Connection" "close")] []) 94 case method of 95 "HEAD" = IO.pure t 96 _ = IO.require (IO.transferBytes reader client size) (_ : IO.pure t) 97 _ = Transport.respond client method (HTTP.errorResponse 500 "Internal Server Error\n")) 98 sendDownload _ _ _ _ _ = IO.fail "Invalid snapshot reply" 99 download root limit sink client method target digest = case B.checkContract Store.bundleHashC digest of 100 ((t t), (checked, t)) = IO.scope (owner : IO.bind 101 (prepareDownload owner root limit checked) (reply : case reply of 102 ((t t), (snapshot, t)) = sendDownload sink client method target snapshot 103 _ = downloadError sink client method target 404 "Artifact unavailable\n")) 104 (t, (message, _)) = downloadError sink client method target 400 message 105 _ = IO.fail "Invalid digest contract reply" 106 downloadRoute root limit sink client method target fields bytes = case HTTP.routerMethod method of 107 "GET" = case Store.stripPrefix Store.bundleHashPrefix target of 108 (t digest) = download root limit sink client method target digest 109 _ = IO.bind (Registry.routerV2 root limit sink "GET" target fields bytes) (Transport.respond client method) 110 routed = IO.bind (Registry.routerV2 root limit sink routed target fields bytes) (Transport.respond client method) 111 112 line root limit sink client ((t t), (("POST", ("/_arboricx/bundle", version)), raw)) = 113 headers root limit sink client version raw 114 line root limit sink client parsed = Transport.handleWith (downloadRoute root limit sink) client parsed 115 connection root limit sink client = IO.bind (Transport.recvHeaders client) (reply : case reply of 116 ((t t), (raw, _)) = line root limit sink client (HTTP.parseRequestLine raw) 117 (t, (431, _)) = Transport.respond client "" (HTTP.errorResponse 431 "Request Header Fields Too Large\n") 118 _ = Transport.respond client "" (HTTP.badRequestResponse "Bad Request\n")) 119 120 worker root limit sink [client] = connection root limit sink client 121 worker _ _ _ _ = IO.fail "Invalid connection handoff" 122 settle ticket = IO.require (IO.join ticket) (outcome : case outcome of 123 ["completed" _] = IO.pure t 124 ["failed" _ _] = IO.bind (Registry.writeLogV2 "registry connection failed\n") (_ : IO.pure t) 125 ["cancelled" reason _] = IO.cancelled reason 126 _ = IO.fail "Invalid connection task outcome") 127 drain [] = IO.pure t 128 drain [ticket | rest] = IO.bind (settle ticket) (_ : drain rest) 129 drain _ = IO.fail "Invalid connection tickets" 130 next (t count) = B.just (N.predecessor count) 131 next _ = t 132 -- FIFO bounded admission: consume the oldest ticket before accepting beyond the 133 -- configured window. No discarded tickets, detached tasks or unbounded backlog in 134 -- the language. A slow oldest connection can delay admission; no deadline promise. 135 admit _ _ _ _ _ _ (t 0) tickets = drain tickets 136 admit root limit sink owner listener window remaining tickets = case N.gte (B.length tickets) window of 137 (t t) = case tickets of 138 [ticket | rest] = IO.bind (settle ticket) (_ : admit root limit sink owner listener window remaining rest) 139 _ = IO.fail "Invalid admission window" 140 _ = IO.require (IO.acceptTCP listener owner) (client : IO.require 141 (IO.spawn 1 1 [client] (worker root limit sink)) (spawned : case spawned of 142 [_ ticket] = admit root limit sink owner listener window (next remaining) (B.append tickets [ticket]) 143 _ = IO.fail "Invalid connection spawn reply")) 144 start root limit sink address window remaining ready = case B.checkContract B.nat? limit of 145 ((t t), _) = startWindow root limit sink address window remaining ready 146 _ = IO.fail "Invalid artifact byte limit" 147 startWindow root limit sink address window remaining ready = case B.checkContract B.nat? window of 148 ((t t), _) = case B.and? (N.gte window 1) (N.lte window 256) of 149 (t t) = IO.scope (owner : do IO.require 150 H.runIOV2 (Artifacts.prepareFilesystemV2 root) 151 listener <- IO.listenTCP owner address 128 152 IO.bind (ready listener) (_ : admit root limit sink owner listener window remaining [])) 153 _ = IO.fail "Connection window must be between 1 and 256" 154 _ = IO.fail "Invalid connection window" 155 serve root limit sink address window ready = start root limit sink address window t ready 156 serveCount root limit sink address window count ready = case B.checkContract B.nat? count of 157 ((t t), _) = start root limit sink address window (B.just count) ready 158 _ = IO.fail "Invalid connection count"