From 211e5e160e64bc018af173bf4c71db250df8f92a Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Wed, 15 May 2024 15:46:54 +0100 Subject: [PATCH 1/6] RPC middleware: http cache header module --- src/lib_rpc_http/RPC_middleware.ml | 5 +++++ src/lib_rpc_http/RPC_middleware.mli | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/lib_rpc_http/RPC_middleware.ml b/src/lib_rpc_http/RPC_middleware.ml index e1d03e91cb96..36655d450566 100644 --- a/src/lib_rpc_http/RPC_middleware.ml +++ b/src/lib_rpc_http/RPC_middleware.ml @@ -174,3 +174,8 @@ let proxy_server_query_forwarder ?acl ?ctx ?forwarder_events forwarding_endpoint ?forwarder_events forwarding_endpoint | None -> make_transform_callback ?ctx ?forwarder_events forwarding_endpoint + +module Http_cache_headers = struct + let make ~get_estimated_time_to_next_level:_ callback conn req body = + callback conn req body +end diff --git a/src/lib_rpc_http/RPC_middleware.mli b/src/lib_rpc_http/RPC_middleware.mli index 5c850e308d6f..c33de14504b8 100644 --- a/src/lib_rpc_http/RPC_middleware.mli +++ b/src/lib_rpc_http/RPC_middleware.mli @@ -59,3 +59,12 @@ val rpc_metrics_transform_callback : unit Tezos_rpc.Directory.t -> RPC_server.callback -> RPC_server.callback + +(** A Resto middleware that adds Http cache headers to responses of any block + query. These headers can be used to by Caches to invlidate responses. *) +module Http_cache_headers : sig + val make : + get_estimated_time_to_next_level:(unit -> Ptime.span option Lwt.t) -> + RPC_server.callback -> + RPC_server.callback +end -- GitLab From fbc7395ab9da517c7ba5144435b2169e050128ce Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Wed, 15 May 2024 15:42:55 +0100 Subject: [PATCH 2/6] RPC middleware: implement max age http cache header --- src/lib_rpc_http/RPC_middleware.ml | 75 ++++++++++++++++++++++++++++- src/lib_rpc_http/RPC_middleware.mli | 1 + 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/lib_rpc_http/RPC_middleware.ml b/src/lib_rpc_http/RPC_middleware.ml index 36655d450566..24d0867be554 100644 --- a/src/lib_rpc_http/RPC_middleware.ml +++ b/src/lib_rpc_http/RPC_middleware.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -176,6 +177,76 @@ let proxy_server_query_forwarder ?acl ?ctx ?forwarder_events forwarding_endpoint | None -> make_transform_callback ?ctx ?forwarder_events forwarding_endpoint module Http_cache_headers = struct - let make ~get_estimated_time_to_next_level:_ callback conn req body = - callback conn req body + (* Using Regex to parse the url path is dirty. Instead, we want to re-use + [Block_services.path] to parse the prefix path. Ideally, we want to define + header features as part of a Resto.Service. Unfortunately, Resto needs + to be extended to support either use case. + + TODO: https://gitlab.com/tezos/tezos/-/issues/7339 + Parse URL using Resto path defined in Block_services.path + + TODO: https://gitlab.com/tezos/tezos/-/issues/7297 + Support headers in Resto Services + *) + (* Matches any path with the prefix /chains//blocks/head<*> where + `head<*>` is a valid head alias eg. `head`, `head-10`,`head~123` *) + let block_pattern = + Re.Str.regexp {|/chains/[A-Za-z0-9]+/blocks/head\(\(-\|~\)[0-9]+\)?.*|} + + let is_block_subpath uri = + let path = Uri.path uri in + Re.Str.string_match block_pattern path 0 + + let add_header field value response = + let add f v resp = + let headers = + let hs = Cohttp.Response.headers resp in + Cohttp.Header.add hs f v + in + Cohttp.Response.make + ~status:(Cohttp.Response.status resp) + ~encoding:(Cohttp.Response.encoding resp) + ~version:(Cohttp.Response.version resp) + ~flush:(Cohttp.Response.flush resp) + ~headers + () + in + match response with + | `Response (response, body) -> `Response (add field value response, body) + | `Expert (response, body) -> `Expert (add field value response, body) + + let may_add_max_age get_estimated_time_to_next_level resp = + let open Lwt_syntax in + let* seconds_to_round_end_opt = get_estimated_time_to_next_level () in + match seconds_to_round_end_opt with + | None -> return resp + | Some s -> + (* `floor` the value to ensure data stored in caches are always + correct. This means a potential increase in cache misses at + the end of a round. The value needs to be truncated becuase + max-age expects a integer. *) + let s = Float.floor (Ptime.Span.to_float_s s) in + if s = 0. then return resp + else + return + @@ add_header + "cache-control" + (Format.sprintf "public, max-age=%d" (int_of_float s)) + resp + + let make ~get_estimated_time_to_next_level callback conn req body = + let open Lwt_syntax in + let* (resp : Cohttp_lwt_unix.Server.response_action) = + callback conn req body + in + match resp with + | `Response (cohttp_response, _) -> + let status_code = Cohttp.Response.status cohttp_response in + if Cohttp.Code.(code_of_status status_code |> is_success) then + let uri = Cohttp.Request.uri req in + match is_block_subpath uri with + | true -> may_add_max_age get_estimated_time_to_next_level resp + | false -> return resp + else return resp + | `Expert _ -> return resp end diff --git a/src/lib_rpc_http/RPC_middleware.mli b/src/lib_rpc_http/RPC_middleware.mli index c33de14504b8..6243d3caeec6 100644 --- a/src/lib_rpc_http/RPC_middleware.mli +++ b/src/lib_rpc_http/RPC_middleware.mli @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) -- GitLab From 79ad8af09c00e17775e8dc1c15a983be15b0c731 Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Thu, 16 May 2024 10:04:09 +0100 Subject: [PATCH 3/6] RPC: add feature flag for http cache headers --- src/lib_node_config/config_file.ml | 50 +++++++++++++++++++++++++---- src/lib_node_config/config_file.mli | 3 ++ src/lib_node_config/shared_arg.ml | 16 ++++++++- src/lib_node_config/shared_arg.mli | 4 +++ 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/src/lib_node_config/config_file.ml b/src/lib_node_config/config_file.ml index 02b4be551dfc..62af1dc42fd3 100644 --- a/src/lib_node_config/config_file.ml +++ b/src/lib_node_config/config_file.ml @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2019-2020 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -381,6 +382,7 @@ and rpc = { acl : RPC_server.Acl.policy; media_type : Media_type.Command_line.t; max_active_rpc_connections : RPC_server.Max_active_rpc_connections.t; + enable_http_cache_headers : bool; } and tls = {cert : string; key : string} @@ -410,6 +412,7 @@ let default_rpc = acl = RPC_server.Acl.empty_policy; media_type = Media_type.Command_line.Any; max_active_rpc_connections = default_max_active_rpc_connections; + enable_http_cache_headers = false; } let default_disable_config_validation = false @@ -581,6 +584,7 @@ let rpc : rpc Data_encoding.t = acl; media_type; max_active_rpc_connections; + enable_http_cache_headers; } -> let cert, key = match tls with @@ -591,14 +595,24 @@ let rpc : rpc Data_encoding.t = match external_listen_addrs with [] -> None | v -> Some v in ( (Some listen_addrs, external_listen_addrs, None, cors_origins), - (cors_headers, cert, key, acl, media_type, max_active_rpc_connections) - )) + ( cors_headers, + cert, + key, + acl, + media_type, + max_active_rpc_connections, + enable_http_cache_headers ) )) (fun ( ( listen_addrs, external_listen_addrs, legacy_listen_addr, cors_origins ), - (cors_headers, cert, key, acl, media_type, max_active_rpc_connections) - ) -> + ( cors_headers, + cert, + key, + acl, + media_type, + max_active_rpc_connections, + enable_http_cache_headers ) ) -> let tls = match (cert, key) with | None, _ | _, None -> None @@ -628,6 +642,7 @@ let rpc : rpc Data_encoding.t = acl; media_type; max_active_rpc_connections; + enable_http_cache_headers; }) (merge_objs (obj4 @@ -654,7 +669,7 @@ let rpc : rpc Data_encoding.t = https://en.wikipedia.org/wiki/Cross-origin_resource_sharing." (list string) default_rpc.cors_origins)) - (obj6 + (obj7 (dft "cors-headers" ~description: @@ -685,7 +700,23 @@ let rpc : rpc Data_encoding.t = ~description: "The maximum number of active connections per RPC endpoint." RPC_server.Max_active_rpc_connections.encoding - default_rpc.max_active_rpc_connections))) + default_rpc.max_active_rpc_connections) + (dft + "enable-http-cache-headers" + ~description: + "Enables HTTP cache headers in the RPC response. When enabled, \ + 'Cache-control' will be present with 'max-age' in the response \ + header of relative queries (eg. head, head-n, head~n). The \ + 'max-age' value indicates the duration of which the returned \ + response is cacheable. It is an estimate of the remaining \ + duration of the current round based on when the block was \ + forged. Enabling this feature adds a performance overhead to \ + all queries hence you should only do so if you are running the \ + RPC server behind a caching server. The feature is implemented \ + based on RFC9111 hence useful for reverse proxies with \ + auto-caching mechanism." + bool + default_rpc.enable_http_cache_headers))) let rpc_encoding = rpc @@ -886,7 +917,8 @@ let update ?(disable_config_validation = false) ?data_dir ?min_connections ?(disable_mempool = default_p2p.disable_mempool) ?(enable_testchain = default_p2p.enable_testchain) ?(cors_origins = []) ?(cors_headers = []) ?rpc_tls ?log_output ?log_coloring - ?synchronisation_threshold ?history_mode ?network ?latency cfg = + ?synchronisation_threshold ?history_mode ?network ?latency + ?enable_http_cache_headers cfg = let open Lwt_result_syntax in let disable_config_validation = cfg.disable_config_validation || disable_config_validation @@ -969,6 +1001,10 @@ let update ?(disable_config_validation = false) ?data_dir ?min_connections acl; media_type; max_active_rpc_connections; + enable_http_cache_headers = + Option.value + ~default:cfg.rpc.enable_http_cache_headers + enable_http_cache_headers; } and metrics_addr = unopt_list ~default:cfg.metrics_addr metrics_addr and log : Logs_simple_config.cfg = diff --git a/src/lib_node_config/config_file.mli b/src/lib_node_config/config_file.mli index f440c67505c4..e2169148c46e 100644 --- a/src/lib_node_config/config_file.mli +++ b/src/lib_node_config/config_file.mli @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2019-2020 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -82,6 +83,7 @@ and rpc = { acl : RPC_server.Acl.policy; media_type : Media_type.Command_line.t; max_active_rpc_connections : RPC_server.Max_active_rpc_connections.t; + enable_http_cache_headers : bool; } and tls = {cert : string; key : string} @@ -140,6 +142,7 @@ val update : ?history_mode:History_mode.t -> ?network:blockchain_network -> ?latency:int -> + ?enable_http_cache_headers:bool -> t -> t tzresult Lwt.t diff --git a/src/lib_node_config/shared_arg.ml b/src/lib_node_config/shared_arg.ml index 47b566198cf2..672d6b3bb6aa 100644 --- a/src/lib_node_config/shared_arg.ml +++ b/src/lib_node_config/shared_arg.ml @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2019-2021 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -70,6 +71,7 @@ type t = { metrics_addr : string list; operation_metadata_size_limit : Shell_limits.operation_metadata_size_limit option; + enable_http_cache_headers : bool option; } type error += @@ -190,7 +192,8 @@ let wrap data_dir config_file network connections max_download_speed external_rpc_listen_addrs rpc_tls cors_origins cors_headers log_output log_coloring history_mode synchronisation_threshold latency disable_config_validation allow_all_rpc media_type - max_active_rpc_connections metrics_addr operation_metadata_size_limit = + max_active_rpc_connections metrics_addr operation_metadata_size_limit + enable_http_cache_headers = let actual_data_dir = Option.value ~default:Config_file.default_data_dir data_dir in @@ -202,6 +205,9 @@ let wrap data_dir config_file network connections max_download_speed let rpc_tls = Option.map (fun (cert, key) -> {Config_file.cert; key}) rpc_tls in + let enable_http_cache_headers = + if enable_http_cache_headers then Some true else None + in { disable_config_validation; data_dir; @@ -239,6 +245,7 @@ let wrap data_dir config_file network connections max_download_speed max_active_rpc_connections; metrics_addr; operation_metadata_size_limit; + enable_http_cache_headers; } let process_command run = @@ -744,6 +751,10 @@ module Term = struct Config_file.default_max_active_rpc_connections & info ~docs ~doc ~docv:"NUM" ["max-active-rpc-connections"]) + let enable_http_cache_headers = + let doc = "Enables HTTP cache headers in the RPC response" in + Arg.(value & flag & info ~docs ~doc ["enable-http-cache-headers"]) + (* Args. *) let args = @@ -758,6 +769,7 @@ module Term = struct $ log_output $ log_coloring $ history_mode $ synchronisation_threshold $ latency $ disable_config_validation $ allow_all_rpc $ media_type $ max_active_rpc_connections $ metrics_addr $ operation_metadata_size_limit + $ enable_http_cache_headers end let read_config_file args = @@ -905,6 +917,7 @@ let patch_config ?(may_override_network = false) ?(emit = Event.emit) max_active_rpc_connections; metrics_addr; operation_metadata_size_limit; + enable_http_cache_headers; } = args in @@ -1062,6 +1075,7 @@ let patch_config ?(may_override_network = false) ?(emit = Event.emit) ?synchronisation_threshold ?history_mode ?latency + ?enable_http_cache_headers cfg let read_and_patch_config_file ?may_override_network ?emit diff --git a/src/lib_node_config/shared_arg.mli b/src/lib_node_config/shared_arg.mli index 0212f58b336e..e8ba68a805a1 100644 --- a/src/lib_node_config/shared_arg.mli +++ b/src/lib_node_config/shared_arg.mli @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2019-2021 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -86,6 +87,9 @@ type t = { operation_metadata_size_limit : Shell_limits.operation_metadata_size_limit option; (** maximum operation metadata size allowed to be stored on disk *) + enable_http_cache_headers : bool option; + (** Adds Cache-control header directives to RPC responses for queries + that are relative to the head block. *) } val process_command : unit tzresult Lwt.t -> unit Cmdliner.Term.ret -- GitLab From 6e6d06ca1247021484c3d61989f4528092d3c732 Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Wed, 8 May 2024 13:47:57 +0100 Subject: [PATCH 4/6] Node run: add http cache header middleware to local rpc server --- src/bin_node/node_run_command.ml | 45 +++++++++++++++++++++++++---- src/lib_rpc_http/RPC_middleware.mli | 2 +- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/src/bin_node/node_run_command.ml b/src/bin_node/node_run_command.ml index 7e5e7737dbab..1caeb4214ff1 100644 --- a/src/bin_node/node_run_command.ml +++ b/src/bin_node/node_run_command.ml @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. *) (* Copyright (c) 2019-2021 Nomadic Labs, *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -207,6 +208,14 @@ module Event = struct should be use with care. Please report encountered issues if any." ~level:Warning () + + let enable_http_cache_headers = + declare_0 + ~section + ~name:"enable_http_cache_headers" + ~msg:"HTTP cache headers enabled" + ~level:Notice + () end open Filename.Infix @@ -399,8 +408,10 @@ let sanitize_cors_headers ~default headers = |> String.Set.(union (of_list default)) |> String.Set.elements -(* Launches an RPC server depending on the given server kind *) -let launch_rpc_server (config : Config_file.t) dir rpc_server_kind addr = +(* Launches an RPC server depending on the given server kind. [middleware] can + be provided to transform the callback. *) +let launch_rpc_server ?middleware (config : Config_file.t) dir rpc_server_kind + addr = let open Lwt_result_syntax in let rpc_config = config.rpc in let media_types = rpc_config.media_type in @@ -457,6 +468,11 @@ let launch_rpc_server (config : Config_file.t) dir rpc_server_kind addr = let callback = RPC_middleware.rpc_metrics_transform_callback ~update_metrics dir callback in + let callback = + match middleware with + | Some middleware -> middleware callback + | None -> callback + in let mode = extract_mode rpc_server_kind in Lwt.catch (fun () -> @@ -491,7 +507,7 @@ type rpc_server_kind = | No_server (* Initializes an RPC server handled by the node main process. *) -let init_local_rpc_server (config : Config_file.t) dir = +let init_local_rpc_server ?middleware (config : Config_file.t) dir = let open Lwt_result_syntax in let* servers = List.concat_map_es @@ -512,7 +528,12 @@ let init_local_rpc_server (config : Config_file.t) dir = `No_password, `Port port ) in - launch_rpc_server config dir (Local (mode, port)) addr) + launch_rpc_server + ?middleware + config + dir + (Local (mode, port)) + addr) addrs) config.rpc.listen_addrs in @@ -654,7 +675,21 @@ let init_rpc (config : Config_file.t) (node : Node.t) internal_events = in let* local_rpc_server = if config.rpc.listen_addrs = [] then return No_server - else init_local_rpc_server config dir + else + let* middleware = + if config.rpc.enable_http_cache_headers then + let*! () = Event.(emit enable_http_cache_headers ()) in + let http_cache_headers_middleware = + let Http_cache_headers.{get_estimated_time_to_next_level} = + Node.http_cache_header_tools node + in + RPC_middleware.Http_cache_headers.make + ~get_estimated_time_to_next_level + in + return_some http_cache_headers_middleware + else return_none + in + init_local_rpc_server ?middleware config dir in (* Start RPC process only when at least one listen addr is given. *) let* rpc_server = diff --git a/src/lib_rpc_http/RPC_middleware.mli b/src/lib_rpc_http/RPC_middleware.mli index 6243d3caeec6..6087590fd51b 100644 --- a/src/lib_rpc_http/RPC_middleware.mli +++ b/src/lib_rpc_http/RPC_middleware.mli @@ -62,7 +62,7 @@ val rpc_metrics_transform_callback : RPC_server.callback (** A Resto middleware that adds Http cache headers to responses of any block - query. These headers can be used to by Caches to invlidate responses. *) + query. These headers can be used to by Caches to invalidate responses. *) module Http_cache_headers : sig val make : get_estimated_time_to_next_level:(unit -> Ptime.span option Lwt.t) -> -- GitLab From 12a04ce4d40b1c2a6661c791088241938497d424 Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Mon, 24 Jun 2024 17:30:54 +0100 Subject: [PATCH 5/6] Tezt lib: expose headers in tezt rpc call_raw and add enable http cache headers to node arguments --- tezt/lib_tezos/RPC_core.ml | 11 +++++++++-- tezt/lib_tezos/RPC_core.mli | 4 ++++ tezt/lib_tezos/node.ml | 8 ++++++-- tezt/lib_tezos/node.mli | 1 + tezt/tests/storage_snapshots.ml | 3 ++- 5 files changed, 22 insertions(+), 5 deletions(-) diff --git a/tezt/lib_tezos/RPC_core.ml b/tezt/lib_tezos/RPC_core.ml index 7599be341598..11d69e380d75 100644 --- a/tezt/lib_tezos/RPC_core.ml +++ b/tezt/lib_tezos/RPC_core.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -51,6 +52,8 @@ type 'result t = { decode : JSON.t -> 'result; } +module HeaderMap = Map.Make (String) + let make ?data ?(query_string = []) verb path decode = {verb; path; query_string; data; decode} @@ -59,7 +62,7 @@ let decode_raw ?(origin = "RPC response") rpc raw = let decode rpc json = rpc.decode json -type 'a response = {body : 'a; code : int} +type 'a response = {body : 'a; code : int; headers : string HeaderMap.t} let check_string_response ?(body_rex = "") ~code (response : string response) = Check.( @@ -159,7 +162,11 @@ let call_raw ?rpc_hooks ?(log_request = true) ?(log_response_status = true) rpc_hooks in if log_response_body then Log.debug ~prefix:"RPC" "%s" body ; - return {body; code = Cohttp.Code.code_of_status response.status} + let headers = + Cohttp.Header.to_list (Cohttp.Response.headers response) + |> List.to_seq |> HeaderMap.of_seq + in + return {body; code = Cohttp.Code.code_of_status response.status; headers} let call_json ?rpc_hooks ?log_request ?log_response_status ?(log_response_body = true) endpoint rpc = diff --git a/tezt/lib_tezos/RPC_core.mli b/tezt/lib_tezos/RPC_core.mli index bf71877cef68..8791baab5131 100644 --- a/tezt/lib_tezos/RPC_core.mli +++ b/tezt/lib_tezos/RPC_core.mli @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -34,6 +35,8 @@ type data = Data of JSON.u | File of string (** Convert verb to string. *) val show_verb : verb -> string +module HeaderMap : module type of Map.Make (String) + (** RPC descriptions. ['result] is the type of values returned by the RPC after decoding. *) @@ -89,6 +92,7 @@ val decode : 'result t -> JSON.t -> 'result type 'a response = { body : 'a; (** Response body. *) code : int; (** Status code (e.g. 200 for OK, 404 for Not Found). *) + headers : string HeaderMap.t; (** Response headers *) } (** [check_string_response ?body_rex ~code response] verifies that the given diff --git a/tezt/lib_tezos/node.ml b/tezt/lib_tezos/node.ml index 57f7b56a15ff..1af837e65d82 100644 --- a/tezt/lib_tezos/node.ml +++ b/tezt/lib_tezos/node.ml @@ -59,6 +59,7 @@ type argument = | RPC_additional_addr of string | RPC_additional_addr_external of string | Max_active_rpc_connections of int + | Enable_http_cache_headers let make_argument = function | Network x -> ["--network"; x] @@ -92,6 +93,7 @@ let make_argument = function | RPC_additional_addr_external addr -> ["--external-rpc-addr"; addr] | Max_active_rpc_connections n -> ["--max-active-rpc-connections"; string_of_int n] + | Enable_http_cache_headers -> ["--enable-http-cache-headers"] let make_arguments arguments = List.flatten (List.map make_argument arguments) @@ -114,7 +116,8 @@ let is_redundant = function | Media_type _, Media_type _ | Metadata_size_limit _, Metadata_size_limit _ | Version, Version - | Max_active_rpc_connections _, Max_active_rpc_connections _ -> + | Max_active_rpc_connections _, Max_active_rpc_connections _ + | Enable_http_cache_headers, Enable_http_cache_headers -> true | Metrics_addr addr1, Metrics_addr addr2 -> addr1 = addr2 | Peer peer1, Peer peer2 -> peer1 = peer2 @@ -139,7 +142,8 @@ let is_redundant = function | RPC_additional_addr _, _ | RPC_additional_addr_external _, _ | Version, _ - | Max_active_rpc_connections _, _ -> + | Max_active_rpc_connections _, _ + | Enable_http_cache_headers, _ -> false (* Some arguments should not be written in the config file by [Node.init] diff --git a/tezt/lib_tezos/node.mli b/tezt/lib_tezos/node.mli index 99cc1d2b805a..b6dcacb3b4cf 100644 --- a/tezt/lib_tezos/node.mli +++ b/tezt/lib_tezos/node.mli @@ -99,6 +99,7 @@ type argument = | RPC_additional_addr of string (** [--rpc-addr] *) | RPC_additional_addr_external of string (** [--external-rpc-addr] *) | Max_active_rpc_connections of int (** [--max-active-rpc-connections] *) + | Enable_http_cache_headers (** [--enable-http-cache-headers] *) (** A TLS configuration for the node: paths to a [.crt] and a [.key] file. diff --git a/tezt/tests/storage_snapshots.ml b/tezt/tests/storage_snapshots.ml index 2e4dad3d3c90..3e123e6930d5 100644 --- a/tezt/tests/storage_snapshots.ml +++ b/tezt/tests/storage_snapshots.ml @@ -2,6 +2,7 @@ (* *) (* Open Source License *) (* Copyright (c) 2022 Nomadic Labs *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -131,7 +132,7 @@ let check_blocks_availability node ~history_mode ~head ~savepoint ~caboose = Node.RPC.(call node @@ get_chain_block_header ~block ()) in (* Expects failure, as the metadata must not be stored. *) - let* {body; code} = + let* {body; code; headers = _} = Node.RPC.(call_json node @@ get_chain_block_metadata ~block ()) in (* In the client, attempting to retrieve missing metadata outputs: -- GitLab From 812496780542e79e466680a9f175c2623c44c52c Mon Sep 17 00:00:00 2001 From: Ryan Tan Date: Thu, 27 Jun 2024 11:51:46 +0100 Subject: [PATCH 6/6] Tezt: add tests for http cache header feature --- src/lib_rpc_http/RPC_middleware.ml | 6 +- src/lib_rpc_http/RPC_middleware.mli | 2 +- tezt/tests/http_cache_headers.ml | 93 +++++++++++++++++++++++++++++ tezt/tests/main.ml | 2 + 4 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 tezt/tests/http_cache_headers.ml diff --git a/src/lib_rpc_http/RPC_middleware.ml b/src/lib_rpc_http/RPC_middleware.ml index 24d0867be554..f08381393b34 100644 --- a/src/lib_rpc_http/RPC_middleware.ml +++ b/src/lib_rpc_http/RPC_middleware.ml @@ -221,10 +221,10 @@ module Http_cache_headers = struct match seconds_to_round_end_opt with | None -> return resp | Some s -> - (* `floor` the value to ensure data stored in caches are always + (* `floor` the value to ensure data stored in caches is always correct. This means a potential increase in cache misses at - the end of a round. The value needs to be truncated becuase - max-age expects a integer. *) + the end of a round. The value needs to be truncated because + max-age expects an integer. *) let s = Float.floor (Ptime.Span.to_float_s s) in if s = 0. then return resp else diff --git a/src/lib_rpc_http/RPC_middleware.mli b/src/lib_rpc_http/RPC_middleware.mli index 6087590fd51b..376cb2bc5d56 100644 --- a/src/lib_rpc_http/RPC_middleware.mli +++ b/src/lib_rpc_http/RPC_middleware.mli @@ -62,7 +62,7 @@ val rpc_metrics_transform_callback : RPC_server.callback (** A Resto middleware that adds Http cache headers to responses of any block - query. These headers can be used to by Caches to invalidate responses. *) + query. These headers can be used by Caches to invalidate responses. *) module Http_cache_headers : sig val make : get_estimated_time_to_next_level:(unit -> Ptime.span option Lwt.t) -> diff --git a/tezt/tests/http_cache_headers.ml b/tezt/tests/http_cache_headers.ml new file mode 100644 index 000000000000..37b88f3f0cc2 --- /dev/null +++ b/tezt/tests/http_cache_headers.ml @@ -0,0 +1,93 @@ +(*****************************************************************************) +(* *) +(* SPDX-License-Identifier: MIT *) +(* Copyright (c) 2024 TriliTech *) +(* *) +(*****************************************************************************) + +(* Testing + ------- + Component: Http cache headers RPC Middleware + Invocation: dune exec tezt/tests/main.exe -- --file http_cache_headers.ml + Subject: Test Http cache headers RPC middleware behaves correctly +*) + +(** [check_max_age_in_headers ?expects_missing_header ~__LOC__ headers] returns + [unit] or fails if "cache-control: max-age" is missing from [headers]. If + [expects_missing_header] is set to [true], it returns [unit] instead of + failing when "max-age" is missing. *) +let check_max_age_in_headers ?(expects_missing_header = false) ~__LOC__ headers + = + match RPC_core.HeaderMap.find_opt "cache-control" headers with + | None -> + if expects_missing_header then Lwt.return_unit + else Test.fail ~__LOC__ "cache-control header not found" + | Some cache_control -> ( + let cache_control_parts = + List.map + (fun s -> String.trim s) + (String.split_on_char ',' cache_control) + in + let max_age_opt = + List.find_opt + (fun s -> String.starts_with ~prefix:"max-age" s) + cache_control_parts + in + match max_age_opt with + | Some _ -> Lwt.return_unit + | None -> Test.fail ~__LOC__ "max-age not found in cache-control header") + +(* [test_max_age] tests the presence of max-age header field + when the round duration has not yet elapsed and the absence + of the header field when the round duration has elapsed and + no new block has arrived. *) +let test_max_age = + Protocol.register_test + ~__FILE__ + ~title:"max-age header" + ~tags:["rpc"; "middleware"; "http_cache_headers"] + ~supports:(From_protocol 19) + @@ fun protocol -> + Log.info "Initialize client, node and baker" ; + let node = + Node.create + [Connections 0; Synchronisation_threshold 0; Enable_http_cache_headers] + in + let http_cache_headers_enabled_event = + Node.wait_for node "enable_http_cache_headers.v0" Option.some + in + let* () = Node.run node [] in + let* () = Node.wait_for_ready node in + let* client = Client.init ~endpoint:(Client.Node node) () in + let* _ = http_cache_headers_enabled_event in + let delegates = + Array.to_list + @@ Array.map (fun key -> Account.(key.alias)) Account.Bootstrap.keys + in + Log.info "Activate protocol" ; + let block_time = 4 in + let* parameter_file = + Protocol.write_parameter_file + ~base:(Right (protocol, None)) + [(["minimal_block_delay"], `String (string_of_int block_time))] + in + let* () = + Client.activate_protocol ~timestamp:Now ~parameter_file ~protocol client + in + Log.info "Bake and wait for block" ; + let* () = + Client.bake_for_and_wait ~minimal_timestamp:true ~keys:delegates client + in + Log.info "Check max-age is present" ; + let* {headers; _} = Node.RPC.call_raw node (RPC.get_chain_block_hash ()) in + let* () = check_max_age_in_headers ~__LOC__ headers in + Log.info "Check max-age not present after max-age duration" ; + let* () = Lwt_unix.sleep (Float.of_int (block_time + 1)) in + let* {headers; _} = Node.RPC.call_raw node (RPC.get_chain_block_hash ()) in + let* () = + check_max_age_in_headers ~expects_missing_header:true ~__LOC__ headers + in + let* () = Node.terminate node in + unit + +let register ~protocols = test_max_age protocols diff --git a/tezt/tests/main.ml b/tezt/tests/main.ml index b53d629c5dde..88f7d7f8b22f 100644 --- a/tezt/tests/main.ml +++ b/tezt/tests/main.ml @@ -3,6 +3,7 @@ (* Open Source License *) (* Copyright (c) 2021 Nomadic Labs *) (* Copyright (c) 2020 Metastate AG *) +(* Copyright (c) 2024 TriliTech *) (* *) (* Permission is hereby granted, free of charge, to any person obtaining a *) (* copy of this software and associated documentation files (the "Software"),*) @@ -154,6 +155,7 @@ let register_protocol_tests_that_use_supports_correctly () = Gas_bound.register ~protocols ; Global_constants.register ~protocols ; Hash_data.register ~protocols ; + Http_cache_headers.register ~protocols ; Increase_paid_storage.register ~protocols ; Injector_test.register ~protocols ; Large_metadata.register ~protocols ; -- GitLab