aboutsummaryrefslogblamecommitdiffstats
path: root/lib/listener.ml
blob: ba61a504f2acb1db3fbb1001ce9a9bfd2ba051f9 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
             

                           
             

        




                                    














                                                                       




































                                                                                    

                            

                                                     
                                          
 
                      
                     

                                                                         
         






                                                    


                                 

                                                   
                                                                    
 
                                                               
                              
                                                     
 
                                      
                                                 
                                                                                   
                                                 
 

                                                


                                                       
                                                                                       





                                                   
                                        


                             
                                                                 
 


                                                                           

                              
                                                             
                                                                      
                                                        


                            





                                                                                      
             


                                                                                


                        

                                                         
             

                                                                                          
                                                       
                                                                                               
 


                                              
                                      












                                                                               
                                         

                                             

                                                  


                                                                    
                                  

















                                                                                          

                         
                                                     

          
module Make =
functor
  (T : Transport.TRANSPORT)
  (NT : Nt.S)
  ->
  struct
    module type SubS = Sublanguage.S

    module Sess = Session.Make (NT)
    module B    = Branch.Make (NT)

    module Error = struct
      open Condition

      let syntax_error msg =
        condition "syntax-error" msg empty

      let unrecognized_sublanguage tag =
        condition "unrecognized-sublanguage" "Unrecognized sublanguage"
          ("tag" |=| (of_string tag))

      let malformed_expression expr =
        condition "malformed-expression" "Malformed expression"
          ("expression" |=| (of_sexp expr))
    end

    (** Per-connection state: session owns the active branch. The
        branch's [relation_path] becomes the context resolver, so all
        sublanguages address the full RNT namespace rather than a
        single multigroup. **)
    type conn_state = {
      claims  : Nt.claims;
      session : Sess.session;
    }

    (** Build a fresh execution context from the current session
        state. Called at the top of every handle_sublanguage so it
        always reflects the live branch after any prior VCL
        switch. **)
    let make_ctx (state : conn_state) : Sublanguage_context.t =
      let br      = state.session#branch in
      let claims  = state.claims in
      let session = state.session in
      {
        Sublanguage_context.write_handle  = br#branch_handle;
        branch                            = (br :> Sublanguage_context.branch_view);
        resolve                           = br#path;
        switch_branch = fun name ->
          match br#close () with
          | Error e -> Error e
          | Ok () ->
              begin match B.open_branch claims name with
               | Error e ->
                  begin match B.open_branch claims "master" with
                  | Ok mbr -> session#set_branch mbr
                  | Error _ -> ()
                  end;
                  Error e
               | Ok new_br ->
                   session#set_branch new_br;
                   Ok new_br#multigroups
              end
      }

    let read_command input =
      try Ok (Sexplib.Sexp.input_sexp input)
      with Sexplib.Sexp.Parse_error { err_msg; _ } ->
        Error (Error.syntax_error err_msg)

    let sublanguages =
      List.fold_right
        (fun (module Language : SubS) ->
          Utilities.StringMap.add Language.name (module Language : SubS))
        [
          (module Drl.Sublanguage.Make (NT) : SubS);
          (module Ddl.Sublanguage.Make (NT) : SubS);
          (module Dml.Sublanguage.Make (NT) : SubS);
          (module Icl.Sublanguage.Make (NT) : SubS);
          (module Prl.Sublanguage.Make (NT) : SubS);
          (module Scl.Sublanguage.Make (NT) : SubS);
          (module Vcl.Sublanguage.Make (NT) : SubS);
        ]
        Utilities.StringMap.empty

    let find_language tag =
      Utilities.StringMap.find_opt tag sublanguages
      |> Option.to_result ~none:(Error.unrecognized_sublanguage tag)

    let execute_sublanguage ctx expr (module Language : SubS) =
      Language.parse_sexp expr
      |> Utilities.Result.fmap (Language.execute ctx)

    let execute_command ctx = function
      | Sexplib.Sexp.(List [ Atom tag; expr ]) ->
          find_language tag |> Utilities.Result.fmap (execute_sublanguage ctx expr)
      | s -> Error (Error.malformed_expression s)

    let tuple_to_sexp (t : Tuple.materialized) =
      let open Sexplib.Sexp in
      List
        (Tuple.AttributeMap.bindings t.Tuple.attributes
        |> List.map (fun (k, attr) ->
            List [ Atom k; Conventions.AbstractValue.sexp_of_t attr.Attribute.value ]))

    let output_response out_ch sexp =
      let response = Sexplib.Sexp.to_string sexp in
      output_string out_ch response;
      output_string out_ch "\n";
      flush out_ch;
      Utilities.print_with_time response

    let send_error out_ch e =
      output_response out_ch
        Sexplib.Sexp.(List [ Atom "error"; Condition.to_sexp e ])

    (* Serializes a sublanguage result.  [branch] is the write target whose
       tip and name surface in Cursor / Transition responses. *)
    let serialize (branch : B.branch) =
      let open Sexplib.Sexp in
      function
      | Error e -> List [ Atom "error"; Condition.to_sexp e ]
      | Ok (Sublanguage_types.Cursor { cursor_id; rows; has_more }) ->
          let row_sexps = List.map tuple_to_sexp rows in
          List
            [
              Atom "cursor";
              List [ Atom "id";        Atom cursor_id ];
              List [ Atom "rows";      List row_sexps ];
              List [ Atom "row_count"; Atom (string_of_int (List.length row_sexps)) ];
              List [ Atom "has_more";  Atom (string_of_bool has_more) ];
              List [ Atom "snapshot";  Atom branch#tip ];
              List [ Atom "branch";    Atom branch#name ];
            ]
      | Ok (Sublanguage_types.Query _rel) ->
          List [ Atom "error"; Atom "unexpected Query result: use Cursor path" ]
      | Ok (Sublanguage_types.Transition _new_cache) ->
          List
            [
              Atom "ok";
              List [ Atom "snapshot"; Atom branch#tip ];
              List [ Atom "branch";   Atom branch#name ];
            ]
      | Ok (Sublanguage_types.SessionSwitch name) ->
          List [ Atom "ok"; List [ Atom "message"; Atom ("Switched to branch " ^ name) ] ]
      | Ok (Sublanguage_types.CreateMultigroup name) ->
          List [ Atom "ok"; List [ Atom "message"; Atom ("Multigroup " ^ name ^ " created") ] ]

    let handle_sublanguage output state sexp =
      let ctx = make_ctx !state in
      match execute_command ctx sexp with
      | Error e -> send_error output e
      | Ok result ->
          (* Sync updated schema_cache back into the branch mirror on any
             state-advancing result (DDL, ICL, VCL).  For VCL the session
             already holds the new branch; refresh_mg is a no-op on it but
             correct for DDL/ICL which stay on the same branch. *)
          (match result with
           | Sublanguage_types.Transition delta ->
               !state.session#branch#apply_delta delta
           | _ -> ());
          serialize !state.session#branch (Ok result) |> output_response output

    let handle_client connection =
      let input  = T.input connection in
      let output = T.output connection in
      match NT.authenticate Nt.PlainText with
      | Error e ->
          (* FIXME: why is this a syntax error? *)
          send_error output e
      | Ok claims ->
          (match Sess.open_session claims ~branch_name:"master" with
           | Error e ->
               send_error output e
           | Ok sess ->
               let state = ref { claims; session = sess } in
               (try
                 while true do
                   match read_command input with
                   | Error e -> send_error output e
                   | Ok sexp -> handle_sublanguage output state sexp
                 done
               with
               | End_of_file -> ignore (!state.session#close ())
               | e ->
                   ignore (!state.session#close ());
                   Printf.eprintf "Error handling connection: %s" (Printexc.to_string e)))

    let spawn_handler connection =
      Stdlib.Domain.spawn (fun () -> handle_client connection)

    let run transport =
      T.listen transport;
      while true do
        T.accept transport |> spawn_handler |> ignore
      done
  end