aboutsummaryrefslogtreecommitdiffstats
path: root/lib/listener.ml
blob: ba61a504f2acb1db3fbb1001ce9a9bfd2ba051f9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
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