tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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"