Skip to content

Commit bc0ba4e

Browse files
authored
idl/gen_client: Don't specify argument values when they're equal to defaults (#6693)
#6652 added a new parameter to `Host.disable`. Since the method is used during an RPU, when a new client calls an older server unaware of the parameter, this broke it. Add a test reproducing what happens during an RPU and fix the issue in `client.ml`. --- Adds an older `server.ml` and `client.ml` from xapi 25.30.0 (with `server.ml` modified to compile after the XSA-474 interface changes), before `Host.disable` gained the `auto_enable` parameter. Adds compatibility tests verifying that an older client can talk to a newer server and the other way around. Before the fix, both `test_compatibility_with_old_server_*` fail, showing that `auto_enable` in `Host.disable` is an unexpected parameter. This failure is triggered on RPUs, when a newer xapi talks to an older one: [exception] Server_error(MESSAGE_PARAMETER_COUNT_MISMATCH, [ host.disable; 1; 2 ]) So allow `client.ml` to skip specifying an arbitrary number of rightmost arguments if they're all equal to their default values (since arguments are positional, once an argument is not skipped, no arguments to its left can be skipped). Generated code for `host.disable` looks like the following: ``` let session_id = rpc_of_ref_session session_id in let host = rpc_of_ref_host host in let auto_enable = rpc_of_bool auto_enable in let needed_args, _ = List.fold_right2 (fun param default (acc, skipped)-> (* Since arguments are positional, we can only skip specifying an argument that's equal to its default value if all the arguments to its right were also not specified *) if skipped then (match default with | Some default_value when param = default_value -> (acc, true) | _ -> (param::acc, false)) else (param :: acc, false) ) [ session_id; host; auto_enable ] [ None; None; Some (Rpc.Bool true) ] ([], true) in rpc_wrapper rpc "host.disable" needed_args >>= fun x -> return (ignore x) ``` This fixes an issue with `client.ml` always specifying values for new parameters that older `server.ml` did not know about (which happens during an RPU). This makes `test_compatibility_with_old_server_default` pass, so drop the `try with` for it. `test_compatibility_with_old_server_non_default` still fails, indicating that everything works as intended. Fixes: cf5be62 ("host.disable: Add auto_enabled parameter for persistency")
2 parents f8031f5 + 197c319 commit bc0ba4e

File tree

3 files changed

+70
-116
lines changed

3 files changed

+70
-116
lines changed

ocaml/idl/ocaml_backend/gen_client.ml

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,27 +92,34 @@ let ctor_fields (obj : obj) =
9292
(function {DT.qualifier= DT.StaticRO | DT.RW; _} -> true | _ -> false)
9393
(DU.fields_of_obj obj)
9494

95-
(* Compute a message parameter list from a message suitable for the client (only!) *)
96-
let args_of_message ?(expand_record = true) (obj : obj)
95+
(* Compute a list of message parameters and their default values from a
96+
message suitable for the client (only!) *)
97+
let args_of_message_with_default ?(expand_record = true) (obj : obj)
9798
({msg_tag= tag; _} as msg) =
9899
let arg_of_param = function
99-
| {param_type= Record x; _} -> (
100+
| {param_type= Record x; param_default= default; _} -> (
100101
match tag with
101102
| FromObject Make ->
102103
if x <> obj.DT.name then failwith "args_of_message" ;
103104
if expand_record then
104-
List.map param_of_field (ctor_fields obj)
105+
List.map
106+
(fun x -> (x, None))
107+
(List.map param_of_field (ctor_fields obj))
105108
else
106-
[custom _value (Record x)]
109+
[(custom _value (Record x), default)]
107110
| _ ->
108111
failwith "arg_of_param: encountered a Record in an unexpected place"
109112
)
110113
| p ->
111-
[of_param p]
114+
[(of_param p, p.param_default)]
112115
in
113-
let session = if msg.msg_session then [session] else [] in
116+
let session = if msg.msg_session then [(session, None)] else [] in
114117
List.concat (session :: List.map arg_of_param msg.msg_params)
115118

119+
(* Compute a message parameter list from a message suitable for the client (only!) *)
120+
let args_of_message ?(expand_record = true) obj x =
121+
List.map fst (args_of_message_with_default ~expand_record obj x)
122+
116123
let gen_module api : O.Module.t =
117124
(* Generate any additional helper functions for an operation here *)
118125
let helper_record_constructor ~sync (obj : obj) (x : message) =
@@ -148,7 +155,8 @@ let gen_module api : O.Module.t =
148155
in
149156
(* Convert an operation into a Let-binding *)
150157
let operation ~sync (obj : obj) (x : message) =
151-
let args = args_of_message obj x in
158+
let args_with_default = args_of_message_with_default obj x in
159+
let args = List.map fst args_with_default in
152160
let to_rpc (arg : O.param) =
153161
let binding = O.string_of_param arg in
154162
let converter = O.type_of_param arg in
@@ -172,6 +180,31 @@ let gen_module api : O.Module.t =
172180
else
173181
List.map O.string_of_param args
174182
in
183+
let defaults =
184+
List.map
185+
(fun (_, default_value) ->
186+
match default_value with
187+
| Some x ->
188+
Printf.sprintf "Some (%s)" (Datamodel_values.to_ocaml_string x)
189+
| None ->
190+
"None"
191+
)
192+
args_with_default
193+
in
194+
let rightmost_arg_default =
195+
Some true
196+
= List.fold_right
197+
(fun (_, x) rightmost_arg_default ->
198+
match rightmost_arg_default with
199+
| None when Option.is_some x ->
200+
Some true
201+
| Some true ->
202+
Some true
203+
| _ ->
204+
Some false
205+
)
206+
args_with_default None
207+
in
175208
let task = DT.Ref Datamodel_common._task in
176209
let from_xmlrpc t =
177210
match (x.msg_custom_marshaller, t, sync) with
@@ -203,15 +236,41 @@ let gen_module api : O.Module.t =
203236
(List.map to_rpc args
204237
@ [
205238
(if is_ctor then ctor_record else "")
239+
; ( if (not is_ctor) && rightmost_arg_default then
240+
(* Skip specifying arguments which are equal to their default
241+
values. This way, when a newer client talks to an older
242+
server that does not know about a new parameter, it can
243+
silently skip sending it, avoiding an error *)
244+
Printf.sprintf
245+
{|
246+
let needed_args, _ = List.fold_right2
247+
(fun param default (acc, skipped)->
248+
(* Since arguments are positional, we can only skip specifying an
249+
argument that's equal to its default value if all the arguments to
250+
its right were also not specified *)
251+
if skipped then
252+
(match default with
253+
| Some default_value when param = default_value -> (acc, true)
254+
| _ -> (param::acc, false))
255+
else
256+
(param :: acc, false)
257+
) [ %s ] [ %s ] ([], true)
258+
in
259+
|}
260+
(String.concat "; " rpc_args)
261+
(String.concat "; " defaults)
262+
else
263+
Printf.sprintf "let needed_args = [ %s ] in"
264+
(String.concat "; " rpc_args)
265+
)
206266
; Printf.sprintf
207-
"rpc_wrapper rpc %s [ %s ] >>= fun x -> return (%s x)"
267+
"rpc_wrapper rpc %s needed_args >>= fun x -> return (%s x)"
208268
( if sync then
209269
Printf.sprintf "\"%s\"" wire_name
210270
else
211271
Printf.sprintf {|(Printf.sprintf "%%s%s" AQ.async_qualifier)|}
212272
wire_name
213273
)
214-
(String.concat "; " rpc_args)
215274
(from_xmlrpc x.msg_result)
216275
]
217276
)
@@ -227,9 +286,6 @@ let gen_module api : O.Module.t =
227286
obj.messages
228287
in
229288
let fields = fields_of (operations @ helpers) in
230-
(*
231-
let fields = List.map (fun x -> O.Module.Let (operation ~sync obj x)) obj.messages in
232-
*)
233289
O.Module.make ~name:(OU.ocaml_of_obj_name obj.DT.name) ~elements:fields ()
234290
in
235291
let preamble =

ocaml/idl/ocaml_backend/gen_test.ml

Lines changed: 0 additions & 102 deletions
This file was deleted.

quality-gate.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ verify-cert () {
2525
}
2626

2727
mli-files () {
28-
N=462
28+
N=461
2929
X="ocaml/tests"
3030
X+="|ocaml/quicktest"
3131
X+="|ocaml/message-switch/core_test"

0 commit comments

Comments
 (0)