diff --git a/.ci/build-plt-cache.sh b/.ci/build-plt-cache.sh deleted file mode 100755 index 538425932..000000000 --- a/.ci/build-plt-cache.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# This script builds/restores PLT cache files to/from a cache at ~/.pltcache -set -ex - -# "Mix will default to the :dev environment" -MIX_ENV="${MIX_ENV:-dev}" - -mix compile - -# Create the PLT cache directory, if it doesn't already exist -mkdir -p "$HOME"/.pltcache -# Copy the PLT files into the _build directory, if they exist -cp "$HOME"/.pltcache/*-"$MIX_ENV".plt _build/"$MIX_ENV"/ || true - -# Build the PLT cache (uses the existing one if present) -mix dialyzer --plt - -# Copy the PLT files into the cache so they can be used next time -cp _build/"$MIX_ENV"/*-"$MIX_ENV".plt "$HOME"/.pltcache/ diff --git a/.expert/expert.log b/.expert/expert.log new file mode 100644 index 000000000..9122abca0 --- /dev/null +++ b/.expert/expert.log @@ -0,0 +1,4504 @@ +14:16:06.851 [debug] Child {XPLoggerFileBackend, :general_log} of Supervisor Logger.Backends.Supervisor started +Pid: #PID<0.102.0> +Start Call: Logger.Backends.Watcher.start_link({{XPLoggerFileBackend, :general_log}, {XPLoggerFileBackend, :general_log}}) +Restart: :transient +Shutdown: 5000 +Type: :worker +14:16:06.851 [info] Child Logger.Backends.Supervisor of Supervisor Logger.Backends.Internal started +Pid: #PID<0.101.0> +Start Call: Logger.Backends.Supervisor.start_link([{XPLoggerFileBackend, :general_log}]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.851 [info] Child Logger.Backends.Internal of Supervisor Logger.Supervisor started +Pid: #PID<0.97.0> +Start Call: Logger.Backends.Internal.start_link([]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.851 [info] Application logger started at :nonode@nohost +14:16:06.851 [info] Application xp_mime started at :nonode@nohost +14:16:06.851 [info] Application xp_hpax started at :nonode@nohost +14:16:06.851 [info] Application crypto started at :nonode@nohost +14:16:06.851 [info] Application asn1 started at :nonode@nohost +14:16:06.851 [info] Application public_key started at :nonode@nohost +14:16:06.853 [debug] Child :ssl_handler of Supervisor :logger_sup started +Pid: #PID<0.107.0> +Start Call: :logger_olp.start_link(:logger_std_h_ssl_handler, :logger_h_common, %{id: :ssl_handler, module: :logger_std_h, config: %{type: :standard_io, filesync_repeat_interval: :no_repeat}, level: :debug, filters: [], filter_default: :stop, formatter: {:ssl_logger, %{}}}, %{}) +Restart: :temporary +Shutdown: 2000 +Type: :worker +14:16:06.856 [info] Child :ssl_pem_cache of Supervisor :ssl_admin_sup started +Pid: #PID<0.111.0> +Start Call: :ssl_pem_cache.start_link([]) +Restart: :permanent +Shutdown: 4000 +Type: :worker +14:16:06.859 [info] Child :ssl_manager of Supervisor :ssl_admin_sup started +Pid: #PID<0.112.0> +Start Call: :ssl_manager.start_link([]) +Restart: :permanent +Shutdown: 4000 +Type: :worker +14:16:06.860 [info] Child :tls_client_ticket_store of Supervisor :ssl_admin_sup started +Pid: #PID<0.113.0> +Start Call: :tls_client_ticket_store.start_link(1000, 7200) +Restart: :permanent +Shutdown: 4000 +Type: :worker +14:16:06.860 [info] Child :ssl_admin_sup of Supervisor :ssl_sup started +Pid: #PID<0.110.0> +Start Call: :ssl_admin_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.861 [info] Child :tls_connection_sup of Supervisor :tls_sup started +Pid: #PID<0.116.0> +Start Call: :tls_connection_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.862 [info] Child :ssl_listen_tracker_sup of Supervisor :tls_server_sup started +Pid: #PID<0.118.0> +Start Call: :ssl_listen_tracker_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.862 [info] Child :tls_server_session_ticket of Supervisor :tls_server_sup started +Pid: #PID<0.119.0> +Start Call: :tls_server_session_ticket_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.863 [info] Child :ssl_server_session_cache_sup of Supervisor :tls_server_sup started +Pid: #PID<0.120.0> +Start Call: :ssl_server_session_cache_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.864 [info] Child :ssl_upgrade_server_session_cache_sup of Supervisor :tls_server_sup started +Pid: #PID<0.121.0> +Start Call: :ssl_upgrade_server_session_cache_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.864 [info] Child :tls_server_sup of Supervisor :tls_sup started +Pid: #PID<0.117.0> +Start Call: :tls_server_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.864 [info] Child :tls_sup of Supervisor :ssl_connection_sup started +Pid: #PID<0.115.0> +Start Call: :tls_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.864 [info] Child :dtls_connection_sup of Supervisor :dtls_sup started +Pid: #PID<0.123.0> +Start Call: :dtls_connection_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.865 [info] Child :dtls_listener_sup of Supervisor :dtls_server_sup started +Pid: #PID<0.125.0> +Start Call: :dtls_listener_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.866 [info] Child :dtls_server_session_cache_sup of Supervisor :dtls_server_sup started +Pid: #PID<0.126.0> +Start Call: :dtls_server_session_cache_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.866 [info] Child :dtls_server_sup of Supervisor :dtls_sup started +Pid: #PID<0.124.0> +Start Call: :dtls_server_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.866 [info] Child :dtls_sup of Supervisor :ssl_connection_sup started +Pid: #PID<0.122.0> +Start Call: :dtls_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.866 [info] Child :ssl_connection_sup of Supervisor :ssl_sup started +Pid: #PID<0.114.0> +Start Call: :ssl_connection_sup.start_link() +Restart: :permanent +Shutdown: 4000 +Type: :supervisor +14:16:06.866 [info] Application ssl started at :nonode@nohost +14:16:06.866 [info] Application xp_mint started at :nonode@nohost +14:16:06.866 [info] Application xp_nimble_options started at :nonode@nohost +14:16:06.868 [info] Child XPNimblePool.TaskSupervisor of Supervisor #PID<0.132.0> (Supervisor.Default) started +Pid: #PID<0.133.0> +Start Call: Task.Supervisor.start_link([name: XPNimblePool.TaskSupervisor]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.868 [info] Application xp_nimble_pool started at :nonode@nohost +14:16:06.869 [info] Child :telemetry_handler_table of Supervisor :telemetry_sup started +Pid: #PID<0.137.0> +Start Call: :telemetry_handler_table.start_link() +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.869 [info] Application xp_telemetry started at :nonode@nohost +14:16:06.869 [info] Application xp_finch started at :nonode@nohost +14:16:06.883 [info] Child XPReq.Finch.PIDPartition0 of Supervisor XPReq.Finch started +Pid: #PID<0.143.0> +Start Call: Registry.Partition.start_link(XPReq.Finch.PIDPartition0, {:duplicate, XPReq.Finch, 0, 1, XPReq.Finch.KeyPartition0, XPReq.Finch.PIDPartition0, [], false}) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.883 [info] Child XPReq.Finch of Supervisor XPReq.Finch.Supervisor started +Pid: #PID<0.142.0> +Start Call: Registry.start_link([keys: :duplicate, name: XPReq.Finch, meta: [config: %{pools: %{}, registry_name: XPReq.Finch, supervisor_name: XPReq.Finch.PoolSupervisor, manager_name: XPReq.Finch.PoolManager, default_pool_config: %{count: 1, size: 50, mod: XPFinch.HTTP1.Pool, conn_opts: [protocols: [:http1], transport_opts: [keepalive: true, nodelay: true, timeout: 5000], ssl_key_log_file_device: nil], pool_max_idle_time: :infinity, conn_max_idle_time: :infinity, start_pool_metrics?: false}}]]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.883 [info] Child XPReq.Finch.PoolSupervisor of Supervisor XPReq.Finch.Supervisor started +Pid: #PID<0.144.0> +Start Call: DynamicSupervisor.start_link([name: XPReq.Finch.PoolSupervisor, strategy: :one_for_one]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.883 [info] Child XPFinch.PoolManager of Supervisor XPReq.Finch.Supervisor started +Pid: #PID<0.145.0> +Start Call: XPFinch.PoolManager.start_link(%{pools: %{}, registry_name: XPReq.Finch, supervisor_name: XPReq.Finch.PoolSupervisor, manager_name: XPReq.Finch.PoolManager, default_pool_config: %{count: 1, size: 50, mod: XPFinch.HTTP1.Pool, conn_opts: [protocols: [:http1], transport_opts: [keepalive: true, nodelay: true, timeout: 5000], ssl_key_log_file_device: nil], pool_max_idle_time: :infinity, conn_max_idle_time: :infinity, start_pool_metrics?: false}}) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.883 [info] Child XPReq.Finch of Supervisor #PID<0.140.0> (Supervisor.Default) started +Pid: #PID<0.141.0> +Start Call: XPFinch.start_link([name: XPReq.Finch, pools: %{default: [protocols: [:http1]]}]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.883 [info] Child XPReq.FinchSupervisor of Supervisor #PID<0.140.0> (Supervisor.Default) started +Pid: #PID<0.146.0> +Start Call: DynamicSupervisor.start_link([strategy: :one_for_one, name: XPReq.FinchSupervisor]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.883 [info] Child XPReq.Test.Ownership of Supervisor #PID<0.140.0> (Supervisor.Default) started +Pid: #PID<0.147.0> +Start Call: XPReq.Test.Ownership.start_link([name: XPReq.Test.Ownership]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.883 [info] Application xp_req started at :nonode@nohost +14:16:06.883 [info] Application burrito started at :nonode@nohost +14:16:06.884 [info] Application xp_schematic started at :nonode@nohost +14:16:06.884 [info] Application xp_typed_struct started at :nonode@nohost +14:16:06.884 [info] Application xp_gen_lsp started at :nonode@nohost +14:16:06.889 [info] Child XPSnowflake.Generator of Supervisor #PID<0.150.0> (Supervisor.Default) started +Pid: #PID<0.151.0> +Start Call: XPSnowflake.Generator.start_link(0, 1023) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.889 [info] Application xp_snowflake started at :nonode@nohost +14:16:06.889 [info] Application xp_sourceror started at :nonode@nohost +14:16:06.889 [info] Application xp_forge started at :nonode@nohost +14:16:06.889 [info] Application xp_logger_file_backend started at :nonode@nohost +14:16:06.892 [info] Child :ttb_autostart of Supervisor :runtime_tools_sup started +Pid: #PID<0.155.0> +Start Call: :ttb_autostart.start_link() +Restart: :temporary +Shutdown: 3000 +Type: :worker +14:16:06.892 [info] Application runtime_tools started at :nonode@nohost +14:16:06.892 [info] Application et started at :nonode@nohost +14:16:06.892 [info] Application observer started at :nonode@nohost +14:16:06.892 [info] Application xp_path_glob started at :nonode@nohost +14:16:06.902 [info] Starting NodePortMapper on nonode@nohost +14:16:06.902 [info] Child XPForge.NodePortMapper of Supervisor XPExpert.Supervisor started +Pid: #PID<0.159.0> +Start Call: XPForge.NodePortMapper.start_link([]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.903 [info] Child XPForge.Document.Store of Supervisor XPExpert.Supervisor started +Pid: #PID<0.160.0> +Start Call: XPForge.Document.Store.start_link([derive: [analysis: &XPForge.Ast.analyze/1]]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.903 [info] Child XPExpert.ProjectSupervisor of Supervisor XPExpert.Supervisor started +Pid: #PID<0.161.0> +Start Call: DynamicSupervisor.start_link([name: XPExpert.ProjectSupervisor, strategy: :one_for_one]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.903 [info] Child XPExpert.DynamicSupervisor of Supervisor XPExpert.Supervisor started +Pid: #PID<0.162.0> +Start Call: DynamicSupervisor.start_link([name: XPExpert.DynamicSupervisor]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.904 [info] Child XPGenLSP.Assigns of Supervisor XPExpert.Supervisor started +Pid: #PID<0.163.0> +Start Call: XPGenLSP.Assigns.start_link([name: XPExpert.Assigns]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.904 [info] Child :expert_task_queue of Supervisor XPExpert.Supervisor started +Pid: #PID<0.164.0> +Start Call: Task.Supervisor.start_link([name: :expert_task_queue]) +Restart: :permanent +Shutdown: :infinity +Type: :supervisor +14:16:06.905 [info] Child XPGenLSP.Buffer of Supervisor XPExpert.Supervisor started +Pid: #PID<0.165.0> +Start Call: XPGenLSP.Buffer.start_link([name: XPExpert.Buffer]) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.905 [debug] [name: XPExpert, buffer: XPExpert.Buffer, task_supervisor: :expert_task_queue, dynamic_supervisor: XPExpert.DynamicSupervisor, assigns: XPExpert.Assigns] +14:16:06.909 [info] Child XPExpert of Supervisor XPExpert.Supervisor started +Pid: #PID<0.166.0> +Start Call: XPExpert.start_link([name: XPExpert, buffer: XPExpert.Buffer, task_supervisor: :expert_task_queue, dynamic_supervisor: XPExpert.DynamicSupervisor, assigns: XPExpert.Assigns]) +Restart: :permanent +Shutdown: 500 +Type: :worker +14:16:06.909 [info] Application xp_expert started at :nonode@nohost +14:16:06.965 [info] Starting project at uri file:///Users/james.cleveland/workspace/surgeventures/grpc +14:16:06.973 [info] Child {XPExpert.Project.Progress, "grpc"} of Supervisor :"grpc::supervisor" started +Pid: #PID<0.171.0> +Start Call: XPExpert.Project.Progress.start_link(%XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.974 [info] Child {XPExpert.EngineSupervisor, "grpc"} of Supervisor :"grpc::supervisor" started +Pid: #PID<0.172.0> +Start Call: XPExpert.EngineSupervisor.start_link(%XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:06.977 [debug] sent request server -> client window/workDoneProgress/create +14:16:06.979 [info] Child XPForge.EPMD of Supervisor :net_sup started +Pid: #PID<0.175.0> +Start Call: XPForge.EPMD.start_link() +Restart: :permanent +Shutdown: 2000 +Type: :worker +14:16:06.979 [info] Child :auth of Supervisor :net_sup started +Pid: #PID<0.176.0> +Start Call: :auth.start_link() +Restart: :permanent +Shutdown: 2000 +Type: :worker +14:16:06.984 [debug] sent notification server -> client $/progress +14:16:06.988 [info] Child :net_kernel of Supervisor :net_sup started +Pid: #PID<0.177.0> +Start Call: :net_kernel.start_link(%{name: :"expert-manager-grpc-62787@127.0.0.1", supervisor: :net_sup_dynamic, net_tickintensity: 4, net_ticktime: 60, name_domain: :longnames, clean_halt: false}) +Restart: :permanent +Shutdown: 2000 +Type: :worker +14:16:06.988 [debug] Child :net_sup_dynamic of Supervisor :kernel_sup started +Pid: #PID<0.174.0> +Start Call: :erl_distribution.start_link(%{name: :"expert-manager-grpc-62787@127.0.0.1", supervisor: :net_sup_dynamic, net_tickintensity: 4, net_ticktime: 60, name_domain: :longnames, clean_halt: false}) +Restart: :permanent +Shutdown: 1000 +Type: :supervisor +14:16:07.016 [debug] handled request client -> server initialize in 101ms +14:16:07.019 [debug] sent request server -> client client/registerCapability +14:16:07.021 [debug] handled notification client -> server workspace/didChangeConfiguration in 5ms +14:16:07.023 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/transport/http2.ex +14:16:07.023 [debug] handled notification client -> server textDocument/didOpen in 5ms +14:16:07.023 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/server.ex +14:16:07.023 [debug] handled notification client -> server textDocument/didOpen in 5ms +14:16:07.024 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/stub.ex +14:16:07.024 [debug] handled notification client -> server textDocument/didOpen in 3ms +14:16:07.024 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs +14:16:07.024 [debug] handled notification client -> server textDocument/didOpen in 3ms +14:16:07.031 [debug] handled notification client -> server initialized in 14ms +14:16:07.376 [debug] sent notification server -> client window/logMessage +14:16:07.377 [debug] sent notification server -> client window/logMessage +14:16:07.377 [debug] sent request server -> client window/workDoneProgress/create +14:16:07.377 [debug] sent notification server -> client $/progress +14:16:07.964 [debug] Building engine: * creating /Users/james.cleveland/.local/share/mise/installs/elixir/1.19.3-otp-28/.mix/archives/hex-2.3.1-otp-28 + +14:16:08.309 [debug] Building engine: * creating /Users/james.cleveland/.local/share/mise/installs/elixir/1.19.3-otp-28/.mix/elixir/1-19-otp-28/rebar3 + +14:16:09.992 [debug] Building engine: Rewriting 20 app files + +14:16:10.029 [debug] Building engine: Rewriting .beam files + +14:16:10.063 [debug] Building engine: Found 0 protocols + +14:16:10.063 [debug] Building engine: Found 1636 app beam files + +14:16:10.075 [debug] Building engine: +14:16:10.075 [debug] Building engine: Applying namespace: 0% complete +14:16:10.076 [debug] Building engine: Applying namespace: 0% complete +14:16:10.076 [debug] Building engine: +14:16:10.076 [debug] Building engine: Applying namespace: 0% complete +14:16:10.076 [debug] Building engine: Applying namespace: 0% complete +14:16:10.076 [debug] Building engine: +14:16:10.077 [debug] Building engine: Applying namespace: 0% complete +14:16:10.077 [debug] Building engine: +14:16:10.077 [debug] Building engine: Applying namespace: 0% complete +14:16:10.077 [debug] Building engine: +14:16:10.077 [debug] Building engine: Applying namespace: 0% complete +14:16:10.078 [debug] Building engine: +14:16:10.078 [debug] Building engine: Applying namespace: 0% complete +14:16:10.078 [debug] Building engine: +14:16:10.079 [debug] Building engine: Applying namespace: 1% complete +14:16:10.079 [debug] Building engine: +14:16:10.079 [debug] Building engine: Applying namespace: 1% complete +14:16:10.079 [debug] Building engine: +14:16:10.079 [debug] Building engine: Applying namespace: 1% complete +14:16:10.081 [debug] Building engine: +14:16:10.081 [debug] Building engine: Applying namespace: 1% complete +14:16:10.083 [debug] Building engine: +14:16:10.083 [debug] Building engine: Applying namespace: 1% complete +14:16:10.092 [debug] Building engine: +14:16:10.092 [debug] Building engine: Applying namespace: 1% complete +14:16:10.092 [debug] Building engine: +14:16:10.093 [debug] Building engine: Applying namespace: 1% complete +14:16:10.094 [debug] Building engine: +14:16:10.094 [debug] Building engine: Applying namespace: 1% complete +14:16:10.097 [debug] Building engine: +14:16:10.099 [debug] Building engine: Applying namespace: 1% complete +14:16:10.101 [debug] Building engine: +14:16:10.101 [debug] Building engine: Applying namespace: 1% complete +14:16:10.102 [debug] Building engine: +14:16:10.103 [debug] Building engine: Applying namespace: 1% complete +14:16:10.103 [debug] Building engine: +14:16:10.104 [debug] Building engine: Applying namespace: 1% complete +14:16:10.104 [debug] Building engine: +14:16:10.105 [debug] Building engine: Applying namespace: 1% complete +14:16:10.105 [debug] Building engine: +14:16:10.108 [debug] Building engine: Applying namespace: 1% complete +14:16:10.108 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 1% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 1% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.128 [debug] Building engine: Applying namespace: 2% complete +14:16:10.128 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 2% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 3% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 3% complete +14:16:10.131 [debug] Building engine: +14:16:10.131 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.133 [debug] Building engine: +14:16:10.133 [debug] Building engine: Applying namespace: 3% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 3% complete +14:16:10.134 [debug] Building engine: Applying namespace: 3% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 3% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 3% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 4% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 4% complete +14:16:10.134 [debug] Building engine: +14:16:10.134 [debug] Building engine: Applying namespace: 4% complete +14:16:10.137 [debug] Building engine: +14:16:10.138 [debug] Building engine: Applying namespace: 4% complete +14:16:10.142 [debug] Building engine: +14:16:10.142 [debug] Building engine: Applying namespace: 4% complete +14:16:10.142 [debug] Building engine: +14:16:10.143 [debug] Building engine: Applying namespace: 4% complete +14:16:10.143 [debug] Building engine: +14:16:10.143 [debug] Building engine: Applying namespace: 4% complete +14:16:10.144 [debug] Building engine: +14:16:10.144 [debug] Building engine: Applying namespace: 4% complete +14:16:10.144 [debug] Building engine: +14:16:10.144 [debug] Building engine: Applying namespace: 4% complete +14:16:10.144 [debug] Building engine: +14:16:10.144 [debug] Building engine: Applying namespace: 4% complete +14:16:10.145 [debug] Building engine: +14:16:10.145 [debug] Building engine: Applying namespace: 4% complete +14:16:10.146 [debug] Building engine: +14:16:10.146 [debug] Building engine: Applying namespace: 4% complete +14:16:10.148 [debug] Building engine: +14:16:10.149 [debug] Building engine: Applying namespace: 4% complete +14:16:10.150 [debug] Building engine: +14:16:10.150 [debug] Building engine: Applying namespace: 4% complete +14:16:10.150 [debug] Building engine: +14:16:10.150 [debug] Building engine: Applying namespace: 4% complete +14:16:10.151 [debug] Building engine: +14:16:10.151 [debug] Building engine: Applying namespace: 4% complete +14:16:10.151 [debug] Building engine: +14:16:10.152 [debug] Building engine: Applying namespace: 5% complete +14:16:10.152 [debug] Building engine: +14:16:10.152 [debug] Building engine: Applying namespace: 5% complete +14:16:10.152 [debug] Building engine: +14:16:10.152 [debug] Building engine: Applying namespace: 5% complete +14:16:10.155 [debug] Building engine: +14:16:10.155 [debug] Building engine: Applying namespace: 5% complete +14:16:10.155 [debug] Building engine: +14:16:10.157 [debug] Building engine: Applying namespace: 5% complete +14:16:10.160 [debug] Building engine: Applying namespace: 5% complete +14:16:10.161 [debug] Building engine: Applying namespace: 5% complete +14:16:10.164 [debug] Building engine: +14:16:10.165 [debug] Building engine: Applying namespace: 5% complete +14:16:10.167 [debug] Building engine: +14:16:10.167 [debug] Building engine: Applying namespace: 5% complete +14:16:10.167 [debug] Building engine: +14:16:10.168 [debug] Building engine: Applying namespace: 5% complete +14:16:10.168 [debug] Building engine: +14:16:10.168 [debug] Building engine: Applying namespace: 5% complete +14:16:10.171 [debug] Building engine: +14:16:10.171 [debug] Building engine: Applying namespace: 5% complete +14:16:10.171 [debug] Building engine: +14:16:10.171 [debug] Building engine: Applying namespace: 5% complete +14:16:10.173 [debug] Building engine: +14:16:10.173 [debug] Building engine: Applying namespace: 5% complete +14:16:10.173 [debug] Building engine: +14:16:10.173 [debug] Building engine: Applying namespace: 5% complete +14:16:10.173 [debug] Building engine: Applying namespace: 5% complete +14:16:10.175 [debug] Building engine: +14:16:10.175 [debug] Building engine: Applying namespace: 6% complete +14:16:10.185 [debug] Building engine: +14:16:10.185 [debug] Building engine: Applying namespace: 6% complete +14:16:10.185 [debug] Building engine: +14:16:10.186 [debug] Building engine: Applying namespace: 6% complete +14:16:10.187 [debug] Building engine: +14:16:10.187 [debug] Building engine: Applying namespace: 6% complete +14:16:10.187 [debug] Building engine: +14:16:10.187 [debug] Building engine: Applying namespace: 6% complete +14:16:10.193 [debug] Building engine: +14:16:10.195 [debug] Building engine: Applying namespace: 6% complete +14:16:10.197 [debug] Building engine: +14:16:10.198 [debug] Building engine: Applying namespace: 6% complete +14:16:10.199 [debug] Building engine: +14:16:10.199 [debug] Building engine: Applying namespace: 6% complete +14:16:10.202 [debug] Building engine: +14:16:10.202 [debug] Building engine: Applying namespace: 6% complete +14:16:10.212 [debug] Building engine: Applying namespace: 6% complete +14:16:10.213 [debug] Building engine: Applying namespace: 6% complete +14:16:10.213 [debug] Building engine: +14:16:10.214 [debug] Building engine: Applying namespace: 6% complete +14:16:10.214 [debug] Building engine: +14:16:10.214 [debug] Building engine: Applying namespace: 6% complete +14:16:10.214 [debug] Building engine: +14:16:10.214 [debug] Building engine: Applying namespace: 6% complete +14:16:10.215 [debug] Building engine: +14:16:10.215 [debug] Building engine: Applying namespace: 6% complete +14:16:10.224 [debug] Building engine: Applying namespace: 6% complete +14:16:10.224 [debug] Building engine: +14:16:10.224 [debug] Building engine: Applying namespace: 6% complete +14:16:10.236 [debug] Building engine: +14:16:10.237 [debug] Building engine: Applying namespace: 7% complete +14:16:10.237 [debug] Building engine: +14:16:10.240 [debug] Building engine: Applying namespace: 7% complete +14:16:10.241 [debug] Building engine: +14:16:10.241 [debug] Building engine: Applying namespace: 7% complete +14:16:10.242 [debug] Building engine: +14:16:10.243 [debug] Building engine: Applying namespace: 7% complete +14:16:10.245 [debug] Building engine: +14:16:10.245 [debug] Building engine: Applying namespace: 7% complete +14:16:10.246 [debug] Building engine: +14:16:10.246 [debug] Building engine: Applying namespace: 7% complete +14:16:10.246 [debug] Building engine: +14:16:10.247 [debug] Building engine: Applying namespace: 7% complete +14:16:10.249 [debug] Building engine: +14:16:10.250 [debug] Building engine: Applying namespace: 7% complete +14:16:10.251 [debug] Building engine: +14:16:10.251 [debug] Building engine: Applying namespace: 7% complete +14:16:10.252 [debug] Building engine: +14:16:10.253 [debug] Building engine: Applying namespace: 7% complete +14:16:10.255 [debug] Building engine: +14:16:10.256 [debug] Building engine: Applying namespace: 7% complete +14:16:10.267 [debug] Building engine: +14:16:10.267 [debug] Building engine: Applying namespace: 7% complete +14:16:10.338 [debug] Building engine: +14:16:10.338 [debug] Building engine: Applying namespace: 7% complete +14:16:10.339 [debug] Building engine: Applying namespace: 7% complete +14:16:10.339 [debug] Building engine: Applying namespace: 7% complete Applying namespace: 7% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: Applying namespace: 8% complete +14:16:10.339 [debug] Building engine: +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: Applying namespace: 8% complete +14:16:10.354 [debug] Building engine: +14:16:10.355 [debug] Building engine: Applying namespace: 8% complete +14:16:10.355 [debug] Building engine: +14:16:10.355 [debug] Building engine: Applying namespace: 8% complete +14:16:10.355 [debug] Building engine: +14:16:10.355 [debug] Building engine: Applying namespace: 9% complete +14:16:10.355 [debug] Building engine: +14:16:10.355 [debug] Building engine: Applying namespace: 9% complete +14:16:10.355 [debug] Building engine: +14:16:10.355 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.378 [debug] Building engine: +14:16:10.378 [debug] Building engine: Applying namespace: 9% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 9% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 9% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 9% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.390 [debug] Building engine: +14:16:10.390 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.394 [debug] Building engine: +14:16:10.394 [debug] Building engine: Applying namespace: 10% complete +14:16:10.398 [debug] Building engine: +14:16:10.399 [debug] Building engine: Applying namespace: 10% complete +14:16:10.404 [debug] Building engine: +14:16:10.404 [debug] Building engine: Applying namespace: 10% complete +14:16:10.405 [debug] Building engine: +14:16:10.405 [debug] Building engine: Applying namespace: 10% complete +14:16:10.406 [debug] Building engine: +14:16:10.407 [debug] Building engine: Applying namespace: 11% complete +14:16:10.407 [debug] Building engine: +14:16:10.407 [debug] Building engine: Applying namespace: 11% complete +14:16:10.411 [debug] Building engine: +14:16:10.412 [debug] Building engine: Applying namespace: 11% complete +14:16:10.412 [debug] Building engine: +14:16:10.413 [debug] Building engine: Applying namespace: 11% complete +14:16:10.413 [debug] Building engine: +14:16:10.413 [debug] Building engine: Applying namespace: 11% complete +14:16:10.414 [debug] Building engine: +14:16:10.415 [debug] Building engine: Applying namespace: 11% complete +14:16:10.415 [debug] Building engine: +14:16:10.415 [debug] Building engine: Applying namespace: 11% complete +14:16:10.416 [debug] Building engine: +14:16:10.416 [debug] Building engine: Applying namespace: 11% complete +14:16:10.420 [debug] Building engine: +14:16:10.420 [debug] Building engine: Applying namespace: 11% complete +14:16:10.421 [debug] Building engine: +14:16:10.421 [debug] Building engine: Applying namespace: 11% complete +14:16:10.425 [debug] Building engine: +14:16:10.425 [debug] Building engine: Applying namespace: 11% complete +14:16:10.429 [debug] Building engine: +14:16:10.429 [debug] Building engine: Applying namespace: 11% complete +14:16:10.432 [debug] Building engine: +14:16:10.432 [debug] Building engine: Applying namespace: 11% complete +14:16:10.437 [debug] Building engine: +14:16:10.437 [debug] Building engine: Applying namespace: 11% complete +14:16:10.438 [debug] Building engine: +14:16:10.438 [debug] Building engine: Applying namespace: 11% complete +14:16:10.438 [debug] Building engine: +14:16:10.439 [debug] Building engine: Applying namespace: 11% complete +14:16:10.439 [debug] Building engine: +14:16:10.439 [debug] Building engine: Applying namespace: 11% complete +14:16:10.440 [debug] Building engine: +14:16:10.440 [debug] Building engine: Applying namespace: 12% complete +14:16:10.441 [debug] Building engine: +14:16:10.441 [debug] Building engine: Applying namespace: 12% complete +14:16:10.443 [debug] Building engine: +14:16:10.443 [debug] Building engine: Applying namespace: 12% complete +14:16:10.444 [debug] Building engine: +14:16:10.445 [debug] Building engine: Applying namespace: 12% complete +14:16:10.445 [debug] Building engine: +14:16:10.445 [debug] Building engine: Applying namespace: 12% complete +14:16:10.446 [debug] Building engine: +14:16:10.446 [debug] Building engine: Applying namespace: 12% complete +14:16:10.448 [debug] Building engine: +14:16:10.448 [debug] Building engine: Applying namespace: 12% complete +14:16:10.450 [debug] Building engine: +14:16:10.450 [debug] Building engine: Applying namespace: 12% complete +14:16:10.453 [debug] Building engine: +14:16:10.453 [debug] Building engine: Applying namespace: 12% complete +14:16:10.454 [debug] Building engine: +14:16:10.454 [debug] Building engine: Applying namespace: 12% complete +14:16:10.456 [debug] Building engine: +14:16:10.457 [debug] Building engine: Applying namespace: 12% complete +14:16:10.460 [debug] Building engine: +14:16:10.460 [debug] Building engine: Applying namespace: 12% complete +14:16:10.461 [debug] Building engine: +14:16:10.463 [debug] Building engine: Applying namespace: 12% complete +14:16:10.463 [debug] Building engine: +14:16:10.464 [debug] Building engine: Applying namespace: 12% complete +14:16:10.464 [debug] Building engine: +14:16:10.464 [debug] Building engine: Applying namespace: 12% complete +14:16:10.465 [debug] Building engine: +14:16:10.465 [debug] Building engine: Applying namespace: 12% complete +14:16:10.465 [debug] Building engine: +14:16:10.465 [debug] Building engine: Applying namespace: 13% complete +14:16:10.466 [debug] Building engine: +14:16:10.466 [debug] Building engine: Applying namespace: 13% complete +14:16:10.466 [debug] Building engine: +14:16:10.466 [debug] Building engine: Applying namespace: 13% complete +14:16:10.468 [debug] Building engine: +14:16:10.468 [debug] Building engine: Applying namespace: 13% complete +14:16:10.468 [debug] Building engine: +14:16:10.468 [debug] Building engine: Applying namespace: 13% complete +14:16:10.469 [debug] Building engine: +14:16:10.470 [debug] Building engine: Applying namespace: 13% complete +14:16:10.475 [debug] Building engine: +14:16:10.475 [debug] Building engine: Applying namespace: 13% complete +14:16:10.476 [debug] Building engine: +14:16:10.477 [debug] Building engine: Applying namespace: 13% complete +14:16:10.482 [debug] Building engine: +14:16:10.482 [debug] Building engine: Applying namespace: 13% complete +14:16:10.483 [debug] Building engine: +14:16:10.483 [debug] Building engine: Applying namespace: 13% complete +14:16:10.483 [debug] Building engine: +14:16:10.483 [debug] Building engine: Applying namespace: 13% complete +14:16:10.484 [debug] Building engine: +14:16:10.484 [debug] Building engine: Applying namespace: 13% complete +14:16:10.484 [debug] Building engine: +14:16:10.485 [debug] Building engine: Applying namespace: 13% complete +14:16:10.485 [debug] Building engine: +14:16:10.486 [debug] Building engine: Applying namespace: 13% complete +14:16:10.489 [debug] Building engine: +14:16:10.489 [debug] Building engine: Applying namespace: 13% complete +14:16:10.490 [debug] Building engine: +14:16:10.490 [debug] Building engine: Applying namespace: 13% complete +14:16:10.497 [debug] Building engine: +14:16:10.498 [debug] Building engine: Applying namespace: 14% complete +14:16:10.498 [debug] Building engine: +14:16:10.498 [debug] Building engine: Applying namespace: 14% complete +14:16:10.498 [debug] Building engine: +14:16:10.498 [debug] Building engine: Applying namespace: 14% complete +14:16:10.499 [debug] Building engine: +14:16:10.499 [debug] Building engine: Applying namespace: 14% complete +14:16:10.499 [debug] Building engine: +14:16:10.499 [debug] Building engine: Applying namespace: 14% complete +14:16:10.500 [debug] Building engine: +14:16:10.500 [debug] Building engine: Applying namespace: 14% complete +14:16:10.504 [debug] Building engine: +14:16:10.505 [debug] Building engine: Applying namespace: 14% complete +14:16:10.505 [debug] Building engine: +14:16:10.506 [debug] Building engine: Applying namespace: 14% complete +14:16:10.506 [debug] Building engine: +14:16:10.506 [debug] Building engine: Applying namespace: 14% complete +14:16:10.508 [debug] Building engine: +14:16:10.511 [debug] Building engine: Applying namespace: 14% complete +14:16:10.513 [debug] Building engine: Applying namespace: 14% complete Applying namespace: 14% complete +14:16:10.514 [debug] Building engine: +14:16:10.514 [debug] Building engine: Applying namespace: 14% complete +14:16:10.514 [debug] Building engine: +14:16:10.514 [debug] Building engine: Applying namespace: 14% complete +14:16:10.515 [debug] Building engine: +14:16:10.515 [debug] Building engine: Applying namespace: 14% complete +14:16:10.516 [debug] Building engine: +14:16:10.516 [debug] Building engine: Applying namespace: 14% complete +14:16:10.520 [debug] Building engine: +14:16:10.520 [debug] Building engine: Applying namespace: 14% complete +14:16:10.525 [debug] Building engine: +14:16:10.526 [debug] Building engine: Applying namespace: 15% complete +14:16:10.527 [debug] Building engine: +14:16:10.527 [debug] Building engine: Applying namespace: 15% complete +14:16:10.528 [debug] Building engine: Applying namespace: 15% complete +14:16:10.529 [debug] Building engine: +14:16:10.529 [debug] Building engine: Applying namespace: 15% complete +14:16:10.529 [debug] Building engine: +14:16:10.530 [debug] Building engine: Applying namespace: 15% complete +14:16:10.532 [debug] Building engine: +14:16:10.533 [debug] Building engine: Applying namespace: 15% complete +14:16:10.533 [debug] Building engine: Applying namespace: 15% complete +14:16:10.534 [debug] Building engine: +14:16:10.534 [debug] Building engine: Applying namespace: 15% complete +14:16:10.534 [debug] Building engine: +14:16:10.535 [debug] Building engine: Applying namespace: 15% complete +14:16:10.535 [debug] Building engine: +14:16:10.535 [debug] Building engine: Applying namespace: 15% complete +14:16:10.536 [debug] Building engine: +14:16:10.536 [debug] Building engine: Applying namespace: 15% complete +14:16:10.539 [debug] Building engine: +14:16:10.539 [debug] Building engine: Applying namespace: 15% complete +14:16:10.540 [debug] Building engine: +14:16:10.541 [debug] Building engine: Applying namespace: 15% complete +14:16:10.541 [debug] Building engine: +14:16:10.542 [debug] Building engine: Applying namespace: 15% complete +14:16:10.542 [debug] Building engine: +14:16:10.542 [debug] Building engine: Applying namespace: 15% complete +14:16:10.542 [debug] Building engine: Applying namespace: 15% complete +14:16:10.542 [debug] Building engine: +14:16:10.543 [debug] Building engine: Applying namespace: 16% complete +14:16:10.544 [debug] Building engine: +14:16:10.544 [debug] Building engine: Applying namespace: 16% complete +14:16:10.545 [debug] Building engine: Applying namespace: 16% complete +14:16:10.547 [debug] Building engine: +14:16:10.547 [debug] Building engine: Applying namespace: 16% complete +14:16:10.547 [debug] Building engine: +14:16:10.547 [debug] Building engine: Applying namespace: 16% complete +14:16:10.547 [debug] Building engine: +14:16:10.548 [debug] Building engine: Applying namespace: 16% complete +14:16:10.548 [debug] Building engine: +14:16:10.548 [debug] Building engine: Applying namespace: 16% complete +14:16:10.548 [debug] Building engine: Applying namespace: 16% complete +14:16:10.553 [debug] Building engine: +14:16:10.553 [debug] Building engine: Applying namespace: 16% complete +14:16:10.553 [debug] Building engine: +14:16:10.553 [debug] Building engine: Applying namespace: 16% complete +14:16:10.554 [debug] Building engine: Applying namespace: 16% complete +14:16:10.554 [debug] Building engine: +14:16:10.554 [debug] Building engine: Applying namespace: 16% complete +14:16:10.555 [debug] Building engine: +14:16:10.555 [debug] Building engine: Applying namespace: 16% complete +14:16:10.557 [debug] Building engine: +14:16:10.558 [debug] Building engine: Applying namespace: 16% complete +14:16:10.558 [debug] Building engine: +14:16:10.558 [debug] Building engine: Applying namespace: 16% complete +14:16:10.558 [debug] Building engine: +14:16:10.558 [debug] Building engine: Applying namespace: 16% complete +14:16:10.560 [debug] Building engine: +14:16:10.560 [debug] Building engine: Applying namespace: 17% complete +14:16:10.564 [debug] Building engine: +14:16:10.564 [debug] Building engine: Applying namespace: 17% complete +14:16:10.565 [debug] Building engine: +14:16:10.565 [debug] Building engine: Applying namespace: 17% complete +14:16:10.565 [debug] Building engine: +14:16:10.565 [debug] Building engine: Applying namespace: 17% complete +14:16:10.565 [debug] Building engine: +14:16:10.566 [debug] Building engine: Applying namespace: 17% complete +14:16:10.566 [debug] Building engine: +14:16:10.566 [debug] Building engine: Applying namespace: 17% complete +14:16:10.566 [debug] Building engine: +14:16:10.566 [debug] Building engine: Applying namespace: 17% complete +14:16:10.567 [debug] Building engine: +14:16:10.568 [debug] Building engine: Applying namespace: 17% complete +14:16:10.568 [debug] Building engine: +14:16:10.568 [debug] Building engine: Applying namespace: 17% complete +14:16:10.573 [debug] Building engine: Applying namespace: 17% complete +14:16:10.573 [debug] Building engine: +14:16:10.573 [debug] Building engine: Applying namespace: 17% complete +14:16:10.583 [debug] Building engine: Applying namespace: 17% complete +14:16:10.584 [debug] Building engine: +14:16:10.584 [debug] Building engine: Applying namespace: 17% complete +14:16:10.592 [debug] Building engine: +14:16:10.592 [debug] Building engine: Applying namespace: 17% complete +14:16:10.603 [debug] Building engine: +14:16:10.603 [debug] Building engine: Applying namespace: 17% complete +14:16:10.611 [debug] Building engine: Applying namespace: 17% complete +14:16:10.614 [debug] Building engine: Applying namespace: 17% complete +14:16:10.614 [debug] Building engine: +14:16:10.615 [debug] Building engine: Applying namespace: 18% complete +14:16:10.617 [debug] Building engine: +14:16:10.617 [debug] Building engine: Applying namespace: 18% complete +14:16:10.618 [debug] Building engine: +14:16:10.619 [debug] Building engine: Applying namespace: 18% complete +14:16:10.633 [debug] Building engine: +14:16:10.634 [debug] Building engine: Applying namespace: 18% complete +14:16:10.636 [debug] Building engine: +14:16:10.636 [debug] Building engine: Applying namespace: 18% complete +14:16:10.642 [debug] Building engine: +14:16:10.642 [debug] Building engine: Applying namespace: 18% complete +14:16:10.662 [debug] Building engine: +14:16:10.663 [debug] Building engine: Applying namespace: 18% complete +14:16:10.665 [debug] Building engine: Applying namespace: 18% complete +14:16:10.671 [debug] Building engine: +14:16:10.672 [debug] Building engine: Applying namespace: 18% complete +14:16:10.689 [debug] Building engine: +14:16:10.689 [debug] Building engine: Applying namespace: 18% complete +14:16:10.690 [debug] Building engine: +14:16:10.690 [debug] Building engine: Applying namespace: 18% complete +14:16:10.727 [debug] Building engine: +14:16:10.727 [debug] Building engine: Applying namespace: 18% complete +14:16:10.742 [debug] Building engine: Applying namespace: 18% complete +14:16:10.760 [debug] Building engine: +14:16:10.760 [debug] Building engine: Applying namespace: 18% complete +14:16:10.760 [debug] Building engine: +14:16:10.761 [debug] Building engine: Applying namespace: 18% complete +14:16:10.765 [debug] Building engine: +14:16:10.765 [debug] Building engine: Applying namespace: 18% complete +14:16:10.765 [debug] Building engine: +14:16:10.765 [debug] Building engine: Applying namespace: 19% complete +14:16:10.772 [debug] Building engine: +14:16:10.772 [debug] Building engine: Applying namespace: 19% complete +14:16:10.780 [debug] Building engine: +14:16:10.780 [debug] Building engine: Applying namespace: 19% complete +14:16:10.784 [debug] Building engine: +14:16:10.784 [debug] Building engine: Applying namespace: 19% complete +14:16:10.785 [debug] Building engine: +14:16:10.785 [debug] Building engine: Applying namespace: 19% complete +14:16:10.785 [debug] Building engine: +14:16:10.786 [debug] Building engine: Applying namespace: 19% complete +14:16:10.790 [debug] Building engine: +14:16:10.790 [debug] Building engine: Applying namespace: 19% complete +14:16:10.796 [debug] Building engine: +14:16:10.797 [debug] Building engine: Applying namespace: 19% complete +14:16:10.803 [debug] Building engine: +14:16:10.803 [debug] Building engine: Applying namespace: 19% complete +14:16:10.812 [debug] Building engine: +14:16:10.812 [debug] Building engine: Applying namespace: 19% complete +14:16:10.823 [debug] Building engine: +14:16:10.823 [debug] Building engine: Applying namespace: 19% complete +14:16:10.826 [debug] Building engine: +14:16:10.827 [debug] Building engine: Applying namespace: 19% complete +14:16:10.829 [debug] Building engine: +14:16:10.830 [debug] Building engine: Applying namespace: 19% complete +14:16:10.833 [debug] Building engine: +14:16:10.833 [debug] Building engine: Applying namespace: 19% complete +14:16:10.833 [debug] Building engine: +14:16:10.834 [debug] Building engine: Applying namespace: 19% complete +14:16:10.835 [debug] Building engine: +14:16:10.836 [debug] Building engine: Applying namespace: 19% complete +14:16:10.836 [debug] Building engine: +14:16:10.838 [debug] Building engine: Applying namespace: 19% complete +14:16:10.838 [debug] Building engine: +14:16:10.839 [debug] Building engine: Applying namespace: 20% complete +14:16:10.842 [debug] Building engine: +14:16:10.843 [debug] Building engine: Applying namespace: 20% complete +14:16:10.844 [debug] Building engine: +14:16:10.844 [debug] Building engine: Applying namespace: 20% complete +14:16:10.857 [debug] Building engine: +14:16:10.858 [debug] Building engine: Applying namespace: 20% complete +14:16:10.858 [debug] Building engine: +14:16:10.859 [debug] Building engine: Applying namespace: 20% complete +14:16:10.860 [debug] Building engine: Applying namespace: 20% complete +14:16:10.862 [debug] Building engine: +14:16:10.862 [debug] Building engine: Applying namespace: 20% complete +14:16:10.872 [debug] Building engine: +14:16:10.873 [debug] Building engine: Applying namespace: 20% complete +14:16:10.873 [debug] Building engine: +14:16:10.874 [debug] Building engine: Applying namespace: 20% complete +14:16:10.887 [debug] Building engine: +14:16:10.887 [debug] Building engine: Applying namespace: 20% complete +14:16:10.888 [debug] Building engine: +14:16:10.888 [debug] Building engine: Applying namespace: 20% complete +14:16:10.893 [debug] Building engine: +14:16:10.893 [debug] Building engine: Applying namespace: 20% complete +14:16:10.893 [debug] Building engine: +14:16:10.893 [debug] Building engine: Applying namespace: 20% complete +14:16:10.897 [debug] Building engine: +14:16:10.898 [debug] Building engine: Applying namespace: 20% complete +14:16:10.901 [debug] Building engine: +14:16:10.901 [debug] Building engine: Applying namespace: 20% complete +14:16:10.902 [debug] Building engine: +14:16:10.902 [debug] Building engine: Applying namespace: 20% complete +14:16:10.914 [debug] Building engine: +14:16:10.914 [debug] Building engine: Applying namespace: 21% complete +14:16:10.917 [debug] Building engine: +14:16:10.918 [debug] Building engine: Applying namespace: 21% complete +14:16:10.920 [debug] Building engine: +14:16:10.921 [debug] Building engine: Applying namespace: 21% complete +14:16:10.931 [debug] Building engine: +14:16:10.931 [debug] Building engine: Applying namespace: 21% complete +14:16:10.931 [debug] Building engine: +14:16:10.932 [debug] Building engine: Applying namespace: 21% complete +14:16:10.935 [debug] Building engine: +14:16:10.935 [debug] Building engine: Applying namespace: 21% complete +14:16:10.936 [debug] Building engine: +14:16:10.937 [debug] Building engine: Applying namespace: 21% complete +14:16:10.938 [debug] Building engine: +14:16:10.938 [debug] Building engine: Applying namespace: 21% complete +14:16:10.938 [debug] Building engine: +14:16:10.939 [debug] Building engine: Applying namespace: 21% complete +14:16:10.939 [debug] Building engine: +14:16:10.940 [debug] Building engine: Applying namespace: 21% complete +14:16:10.942 [debug] Building engine: +14:16:10.942 [debug] Building engine: Applying namespace: 21% complete +14:16:10.944 [debug] Building engine: +14:16:10.944 [debug] Building engine: Applying namespace: 21% complete +14:16:10.945 [debug] Building engine: +14:16:10.945 [debug] Building engine: Applying namespace: 21% complete +14:16:10.953 [debug] Building engine: +14:16:10.953 [debug] Building engine: Applying namespace: 21% complete +14:16:10.963 [debug] Building engine: +14:16:10.963 [debug] Building engine: Applying namespace: 21% complete +14:16:10.963 [debug] Building engine: +14:16:10.967 [debug] Building engine: Applying namespace: 21% complete +14:16:10.972 [debug] Building engine: +14:16:10.972 [debug] Building engine: Applying namespace: 22% complete +14:16:10.973 [debug] Building engine: +14:16:10.973 [debug] Building engine: Applying namespace: 22% complete +14:16:10.973 [debug] Building engine: +14:16:10.974 [debug] Building engine: Applying namespace: 22% complete +14:16:10.978 [debug] Building engine: +14:16:10.978 [debug] Building engine: Applying namespace: 22% complete +14:16:10.978 [debug] Building engine: +14:16:10.979 [debug] Building engine: Applying namespace: 22% complete +14:16:10.979 [debug] Building engine: +14:16:10.980 [debug] Building engine: Applying namespace: 22% complete +14:16:10.985 [debug] Building engine: +14:16:10.986 [debug] Building engine: Applying namespace: 22% complete +14:16:10.988 [debug] Building engine: +14:16:10.990 [debug] Building engine: Applying namespace: 22% complete +14:16:10.991 [debug] Building engine: +14:16:10.991 [debug] Building engine: Applying namespace: 22% complete +14:16:10.992 [debug] Building engine: +14:16:10.992 [debug] Building engine: Applying namespace: 22% complete +14:16:10.992 [debug] Building engine: +14:16:10.993 [debug] Building engine: Applying namespace: 22% complete +14:16:10.994 [debug] Building engine: +14:16:10.995 [debug] Building engine: Applying namespace: 22% complete +14:16:10.996 [debug] Building engine: +14:16:10.996 [debug] Building engine: Applying namespace: 22% complete +14:16:11.000 [debug] Building engine: +14:16:11.000 [debug] Building engine: Applying namespace: 22% complete +14:16:11.003 [debug] Building engine: +14:16:11.003 [debug] Building engine: Applying namespace: 22% complete +14:16:11.003 [debug] Building engine: +14:16:11.004 [debug] Building engine: Applying namespace: 22% complete +14:16:11.005 [debug] Building engine: +14:16:11.005 [debug] Building engine: Applying namespace: 22% complete +14:16:11.007 [debug] Building engine: +14:16:11.007 [debug] Building engine: Applying namespace: 23% complete +14:16:11.009 [debug] Building engine: +14:16:11.009 [debug] Building engine: Applying namespace: 23% complete +14:16:11.016 [debug] Building engine: +14:16:11.016 [debug] Building engine: Applying namespace: 23% complete +14:16:11.019 [debug] Building engine: +14:16:11.020 [debug] Building engine: Applying namespace: 23% complete +14:16:11.020 [debug] Building engine: +14:16:11.020 [debug] Building engine: Applying namespace: 23% complete +14:16:11.023 [debug] Building engine: +14:16:11.023 [debug] Building engine: Applying namespace: 23% complete +14:16:11.026 [debug] Building engine: +14:16:11.026 [debug] Building engine: Applying namespace: 23% complete +14:16:11.033 [debug] Building engine: Applying namespace: 23% complete +14:16:11.033 [debug] Building engine: +14:16:11.033 [debug] Building engine: Applying namespace: 23% complete +14:16:11.035 [debug] Building engine: +14:16:11.035 [debug] Building engine: Applying namespace: 23% complete +14:16:11.036 [debug] Building engine: Applying namespace: 23% complete +14:16:11.036 [debug] Building engine: Applying namespace: 23% complete +14:16:11.036 [debug] Building engine: +14:16:11.037 [debug] Building engine: Applying namespace: 23% complete +14:16:11.038 [debug] Building engine: +14:16:11.038 [debug] Building engine: Applying namespace: 23% complete +14:16:11.038 [debug] Building engine: +14:16:11.038 [debug] Building engine: Applying namespace: 23% complete +14:16:11.040 [debug] Building engine: +14:16:11.040 [debug] Building engine: Applying namespace: 23% complete +14:16:11.045 [debug] Building engine: +14:16:11.045 [debug] Building engine: Applying namespace: 24% complete +14:16:11.046 [debug] Building engine: +14:16:11.046 [debug] Building engine: Applying namespace: 24% complete +14:16:11.046 [debug] Building engine: +14:16:11.046 [debug] Building engine: Applying namespace: 24% complete +14:16:11.048 [debug] Building engine: +14:16:11.048 [debug] Building engine: Applying namespace: 24% complete +14:16:11.050 [debug] Building engine: +14:16:11.050 [debug] Building engine: Applying namespace: 24% complete +14:16:11.051 [debug] Building engine: +14:16:11.051 [debug] Building engine: Applying namespace: 24% complete +14:16:11.055 [debug] Building engine: +14:16:11.056 [debug] Building engine: Applying namespace: 24% complete +14:16:11.057 [debug] Building engine: Applying namespace: 24% complete +14:16:11.059 [debug] Building engine: Applying namespace: 24% complete +14:16:11.064 [debug] Building engine: +14:16:11.064 [debug] Building engine: Applying namespace: 24% complete +14:16:11.068 [debug] Building engine: +14:16:11.068 [debug] Building engine: Applying namespace: 24% complete +14:16:11.068 [debug] Building engine: +14:16:11.069 [debug] Building engine: Applying namespace: 24% complete +14:16:11.069 [debug] Building engine: +14:16:11.069 [debug] Building engine: Applying namespace: 24% complete +14:16:11.069 [debug] Building engine: +14:16:11.070 [debug] Building engine: Applying namespace: 24% complete +14:16:11.070 [debug] Building engine: +14:16:11.070 [debug] Building engine: Applying namespace: 24% complete +14:16:11.072 [debug] Building engine: +14:16:11.072 [debug] Building engine: Applying namespace: 24% complete +14:16:11.082 [debug] Building engine: +14:16:11.083 [debug] Building engine: Applying namespace: 25% complete +14:16:11.083 [debug] Building engine: +14:16:11.083 [debug] Building engine: Applying namespace: 25% complete +14:16:11.084 [debug] Building engine: +14:16:11.085 [debug] Building engine: Applying namespace: 25% complete +14:16:11.086 [debug] Building engine: +14:16:11.086 [debug] Building engine: Applying namespace: 25% complete +14:16:11.087 [debug] Building engine: Applying namespace: 25% complete +14:16:11.087 [debug] Building engine: +14:16:11.088 [debug] Building engine: Applying namespace: 25% complete +14:16:11.091 [debug] Building engine: +14:16:11.091 [debug] Building engine: Applying namespace: 25% complete +14:16:11.096 [debug] Building engine: +14:16:11.096 [debug] Building engine: Applying namespace: 25% complete +14:16:11.096 [debug] Building engine: +14:16:11.096 [debug] Building engine: Applying namespace: 25% complete +14:16:11.097 [debug] Building engine: +14:16:11.097 [debug] Building engine: Applying namespace: 25% complete +14:16:11.099 [debug] Building engine: +14:16:11.099 [debug] Building engine: Applying namespace: 25% complete +14:16:11.101 [debug] Building engine: +14:16:11.101 [debug] Building engine: Applying namespace: 25% complete +14:16:11.102 [debug] Building engine: +14:16:11.102 [debug] Building engine: Applying namespace: 25% complete +14:16:11.102 [debug] Building engine: +14:16:11.103 [debug] Building engine: Applying namespace: 25% complete +14:16:11.103 [debug] Building engine: +14:16:11.104 [debug] Building engine: Applying namespace: 25% complete +14:16:11.109 [debug] Building engine: +14:16:11.109 [debug] Building engine: Applying namespace: 25% complete +14:16:11.112 [debug] Building engine: +14:16:11.112 [debug] Building engine: Applying namespace: 25% complete +14:16:11.112 [debug] Building engine: +14:16:11.113 [debug] Building engine: Applying namespace: 26% complete +14:16:11.114 [debug] Building engine: Applying namespace: 26% complete +14:16:11.114 [debug] Building engine: Applying namespace: 26% complete +14:16:11.115 [debug] Building engine: +14:16:11.116 [debug] Building engine: Applying namespace: 26% complete +14:16:11.116 [debug] Building engine: +14:16:11.116 [debug] Building engine: Applying namespace: 26% complete +14:16:11.117 [debug] Building engine: +14:16:11.117 [debug] Building engine: Applying namespace: 26% complete +14:16:11.119 [debug] Building engine: +14:16:11.120 [debug] Building engine: Applying namespace: 26% complete +14:16:11.120 [debug] Building engine: +14:16:11.120 [debug] Building engine: Applying namespace: 26% complete +14:16:11.120 [debug] Building engine: +14:16:11.122 [debug] Building engine: Applying namespace: 26% complete +14:16:11.122 [debug] Building engine: +14:16:11.123 [debug] Building engine: Applying namespace: 26% complete +14:16:11.126 [debug] Building engine: +14:16:11.126 [debug] Building engine: Applying namespace: 26% complete +14:16:11.127 [debug] Building engine: +14:16:11.128 [debug] Building engine: Applying namespace: 26% complete +14:16:11.128 [debug] Building engine: +14:16:11.128 [debug] Building engine: Applying namespace: 26% complete +14:16:11.130 [debug] Building engine: +14:16:11.131 [debug] Building engine: Applying namespace: 26% complete +14:16:11.131 [debug] Building engine: +14:16:11.131 [debug] Building engine: Applying namespace: 26% complete +14:16:11.132 [debug] Building engine: +14:16:11.132 [debug] Building engine: Applying namespace: 26% complete +14:16:11.132 [debug] Building engine: +14:16:11.133 [debug] Building engine: Applying namespace: 27% complete +14:16:11.133 [debug] Building engine: +14:16:11.133 [debug] Building engine: Applying namespace: 27% complete +14:16:11.134 [debug] Building engine: +14:16:11.134 [debug] Building engine: Applying namespace: 27% complete +14:16:11.134 [debug] Building engine: +14:16:11.135 [debug] Building engine: Applying namespace: 27% complete +14:16:11.137 [debug] Building engine: +14:16:11.137 [debug] Building engine: Applying namespace: 27% complete +14:16:11.137 [debug] Building engine: Applying namespace: 27% complete +14:16:11.139 [debug] Building engine: +14:16:11.139 [debug] Building engine: Applying namespace: 27% complete +14:16:11.143 [debug] Building engine: +14:16:11.143 [debug] Building engine: Applying namespace: 27% complete +14:16:11.158 [debug] Building engine: +14:16:11.159 [debug] Building engine: Applying namespace: 27% complete +14:16:11.160 [debug] Building engine: +14:16:11.161 [debug] Building engine: Applying namespace: 27% complete +14:16:11.163 [debug] Building engine: +14:16:11.163 [debug] Building engine: Applying namespace: 27% complete +14:16:11.166 [debug] Building engine: +14:16:11.166 [debug] Building engine: Applying namespace: 27% complete +14:16:11.167 [debug] Building engine: +14:16:11.167 [debug] Building engine: Applying namespace: 27% complete +14:16:11.170 [debug] Building engine: +14:16:11.171 [debug] Building engine: Applying namespace: 27% complete +14:16:11.174 [debug] Building engine: +14:16:11.175 [debug] Building engine: Applying namespace: 27% complete +14:16:11.176 [debug] Building engine: +14:16:11.177 [debug] Building engine: Applying namespace: 27% complete +14:16:11.177 [debug] Building engine: +14:16:11.178 [debug] Building engine: Applying namespace: 28% complete +14:16:11.178 [debug] Building engine: +14:16:11.179 [debug] Building engine: Applying namespace: 28% complete +14:16:11.179 [debug] Building engine: +14:16:11.179 [debug] Building engine: Applying namespace: 28% complete +14:16:11.180 [debug] Building engine: +14:16:11.180 [debug] Building engine: Applying namespace: 28% complete +14:16:11.180 [debug] Building engine: +14:16:11.180 [debug] Building engine: Applying namespace: 28% complete +14:16:11.182 [debug] Building engine: +14:16:11.182 [debug] Building engine: Applying namespace: 28% complete +14:16:11.183 [debug] Building engine: +14:16:11.184 [debug] Building engine: Applying namespace: 28% complete +14:16:11.184 [debug] Building engine: +14:16:11.184 [debug] Building engine: Applying namespace: 28% complete +14:16:11.184 [debug] Building engine: +14:16:11.184 [debug] Building engine: Applying namespace: 28% complete +14:16:11.185 [debug] Building engine: +14:16:11.185 [debug] Building engine: Applying namespace: 28% complete +14:16:11.194 [debug] Building engine: +14:16:11.195 [debug] Building engine: Applying namespace: 28% complete +14:16:11.201 [debug] Building engine: +14:16:11.201 [debug] Building engine: Applying namespace: 28% complete +14:16:11.208 [debug] Building engine: +14:16:11.208 [debug] Building engine: Applying namespace: 28% complete +14:16:11.209 [debug] Building engine: +14:16:11.209 [debug] Building engine: Applying namespace: 28% complete +14:16:11.211 [debug] Building engine: +14:16:11.211 [debug] Building engine: Applying namespace: 28% complete +14:16:11.212 [debug] Building engine: +14:16:11.212 [debug] Building engine: Applying namespace: 28% complete +14:16:11.212 [debug] Building engine: +14:16:11.213 [debug] Building engine: Applying namespace: 28% complete +14:16:11.213 [debug] Building engine: +14:16:11.213 [debug] Building engine: Applying namespace: 29% complete +14:16:11.216 [debug] Building engine: +14:16:11.216 [debug] Building engine: Applying namespace: 29% complete +14:16:11.220 [debug] Building engine: +14:16:11.221 [debug] Building engine: Applying namespace: 29% complete +14:16:11.223 [debug] Building engine: +14:16:11.224 [debug] Building engine: Applying namespace: 29% complete +14:16:11.224 [debug] Building engine: +14:16:11.225 [debug] Building engine: Applying namespace: 29% complete +14:16:11.225 [debug] Building engine: +14:16:11.225 [debug] Building engine: Applying namespace: 29% complete +14:16:11.226 [debug] Building engine: +14:16:11.227 [debug] Building engine: Applying namespace: 29% complete +14:16:11.227 [debug] Building engine: +14:16:11.227 [debug] Building engine: Applying namespace: 29% complete +14:16:11.228 [debug] Building engine: +14:16:11.228 [debug] Building engine: Applying namespace: 29% complete +14:16:11.228 [debug] Building engine: +14:16:11.228 [debug] Building engine: Applying namespace: 29% complete +14:16:11.231 [debug] Building engine: +14:16:11.232 [debug] Building engine: Applying namespace: 29% complete +14:16:11.232 [debug] Building engine: +14:16:11.232 [debug] Building engine: Applying namespace: 29% complete +14:16:11.233 [debug] Building engine: +14:16:11.233 [debug] Building engine: Applying namespace: 29% complete +14:16:11.233 [debug] Building engine: +14:16:11.233 [debug] Building engine: Applying namespace: 29% complete +14:16:11.234 [debug] Building engine: +14:16:11.234 [debug] Building engine: Applying namespace: 29% complete +14:16:11.235 [debug] Building engine: +14:16:11.235 [debug] Building engine: Applying namespace: 29% complete +14:16:11.235 [debug] Building engine: +14:16:11.235 [debug] Building engine: Applying namespace: 30% complete +14:16:11.235 [debug] Building engine: +14:16:11.235 [debug] Building engine: Applying namespace: 30% complete +14:16:11.236 [debug] Building engine: +14:16:11.236 [debug] Building engine: Applying namespace: 30% complete +14:16:11.237 [debug] Building engine: +14:16:11.237 [debug] Building engine: Applying namespace: 30% complete +14:16:11.238 [debug] Building engine: +14:16:11.238 [debug] Building engine: Applying namespace: 30% complete +14:16:11.239 [debug] Building engine: +14:16:11.241 [debug] Building engine: Applying namespace: 30% complete +14:16:11.241 [debug] Building engine: +14:16:11.241 [debug] Building engine: Applying namespace: 30% complete +14:16:11.241 [debug] Building engine: +14:16:11.241 [debug] Building engine: Applying namespace: 30% complete +14:16:11.242 [debug] Building engine: Applying namespace: 30% complete +14:16:11.242 [debug] Building engine: +14:16:11.242 [debug] Building engine: Applying namespace: 30% complete +14:16:11.242 [debug] Building engine: +14:16:11.242 [debug] Building engine: Applying namespace: 30% complete +14:16:11.242 [debug] Building engine: +14:16:11.243 [debug] Building engine: Applying namespace: 30% complete +14:16:11.243 [debug] Building engine: +14:16:11.243 [debug] Building engine: Applying namespace: 30% complete Applying namespace: 30% complete +14:16:11.244 [debug] Building engine: Applying namespace: 30% complete +14:16:11.245 [debug] Building engine: +14:16:11.245 [debug] Building engine: Applying namespace: 30% complete +14:16:11.245 [debug] Building engine: +14:16:11.245 [debug] Building engine: Applying namespace: 31% complete +14:16:11.246 [debug] Building engine: +14:16:11.246 [debug] Building engine: Applying namespace: 31% complete +14:16:11.246 [debug] Building engine: +14:16:11.247 [debug] Building engine: Applying namespace: 31% complete +14:16:11.249 [debug] Building engine: +14:16:11.249 [debug] Building engine: Applying namespace: 31% complete +14:16:11.250 [debug] Building engine: +14:16:11.250 [debug] Building engine: Applying namespace: 31% complete +14:16:11.251 [debug] Building engine: +14:16:11.251 [debug] Building engine: Applying namespace: 31% complete +14:16:11.253 [debug] Building engine: +14:16:11.253 [debug] Building engine: Applying namespace: 31% complete +14:16:11.253 [debug] Building engine: +14:16:11.253 [debug] Building engine: Applying namespace: 31% complete +14:16:11.253 [debug] Building engine: +14:16:11.256 [debug] Building engine: Applying namespace: 31% complete +14:16:11.256 [debug] Building engine: Applying namespace: 31% complete +14:16:11.257 [debug] Building engine: +14:16:11.257 [debug] Building engine: Applying namespace: 31% complete +14:16:11.257 [debug] Building engine: +14:16:11.257 [debug] Building engine: Applying namespace: 31% complete +14:16:11.257 [debug] Building engine: +14:16:11.258 [debug] Building engine: Applying namespace: 31% complete +14:16:11.258 [debug] Building engine: +14:16:11.258 [debug] Building engine: Applying namespace: 31% complete +14:16:11.258 [debug] Building engine: +14:16:11.259 [debug] Building engine: Applying namespace: 31% complete +14:16:11.259 [debug] Building engine: +14:16:11.260 [debug] Building engine: Applying namespace: 31% complete +14:16:11.261 [debug] Building engine: Applying namespace: 31% complete +14:16:11.261 [debug] Building engine: +14:16:11.261 [debug] Building engine: Applying namespace: 32% complete +14:16:11.261 [debug] Building engine: +14:16:11.261 [debug] Building engine: Applying namespace: 32% complete +14:16:11.263 [debug] Building engine: +14:16:11.264 [debug] Building engine: Applying namespace: 32% complete +14:16:11.264 [debug] Building engine: +14:16:11.264 [debug] Building engine: Applying namespace: 32% complete +14:16:11.265 [debug] Building engine: +14:16:11.265 [debug] Building engine: Applying namespace: 32% complete +14:16:11.265 [debug] Building engine: +14:16:11.266 [debug] Building engine: Applying namespace: 32% complete +14:16:11.266 [debug] Building engine: +14:16:11.266 [debug] Building engine: Applying namespace: 32% complete +14:16:11.266 [debug] Building engine: +14:16:11.267 [debug] Building engine: Applying namespace: 32% complete +14:16:11.267 [debug] Building engine: +14:16:11.267 [debug] Building engine: Applying namespace: 32% complete +14:16:11.268 [debug] Building engine: +14:16:11.268 [debug] Building engine: Applying namespace: 32% complete +14:16:11.268 [debug] Building engine: +14:16:11.268 [debug] Building engine: Applying namespace: 32% complete +14:16:11.268 [debug] Building engine: +14:16:11.269 [debug] Building engine: Applying namespace: 32% complete +14:16:11.269 [debug] Building engine: +14:16:11.269 [debug] Building engine: Applying namespace: 32% complete +14:16:11.269 [debug] Building engine: Applying namespace: 32% complete +14:16:11.269 [debug] Building engine: +14:16:11.277 [debug] Building engine: Applying namespace: 32% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 32% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: Applying namespace: 33% complete +14:16:11.278 [debug] Building engine: +14:16:11.280 [debug] Building engine: Applying namespace: 33% complete +14:16:11.280 [debug] Building engine: +14:16:11.280 [debug] Building engine: Applying namespace: 33% complete +14:16:11.281 [debug] Building engine: +14:16:11.281 [debug] Building engine: Applying namespace: 33% complete +14:16:11.282 [debug] Building engine: +14:16:11.283 [debug] Building engine: Applying namespace: 33% complete +14:16:11.283 [debug] Building engine: +14:16:11.283 [debug] Building engine: Applying namespace: 33% complete +14:16:11.286 [debug] Building engine: +14:16:11.287 [debug] Building engine: Applying namespace: 33% complete +14:16:11.288 [debug] Building engine: +14:16:11.289 [debug] Building engine: Applying namespace: 33% complete +14:16:11.289 [debug] Building engine: +14:16:11.289 [debug] Building engine: Applying namespace: 33% complete +14:16:11.290 [debug] Building engine: +14:16:11.290 [debug] Building engine: Applying namespace: 34% complete +14:16:11.291 [debug] Building engine: +14:16:11.291 [debug] Building engine: Applying namespace: 34% complete +14:16:11.291 [debug] Building engine: +14:16:11.291 [debug] Building engine: Applying namespace: 34% complete +14:16:11.291 [debug] Building engine: +14:16:11.291 [debug] Building engine: Applying namespace: 34% complete +14:16:11.292 [debug] Building engine: +14:16:11.292 [debug] Building engine: Applying namespace: 34% complete +14:16:11.293 [debug] Building engine: +14:16:11.293 [debug] Building engine: Applying namespace: 34% complete +14:16:11.294 [debug] Building engine: +14:16:11.294 [debug] Building engine: Applying namespace: 34% complete +14:16:11.294 [debug] Building engine: +14:16:11.294 [debug] Building engine: Applying namespace: 34% complete +14:16:11.298 [debug] Building engine: +14:16:11.298 [debug] Building engine: Applying namespace: 34% complete +14:16:11.298 [debug] Building engine: +14:16:11.299 [debug] Building engine: Applying namespace: 34% complete +14:16:11.299 [debug] Building engine: +14:16:11.299 [debug] Building engine: Applying namespace: 34% complete +14:16:11.301 [debug] Building engine: +14:16:11.301 [debug] Building engine: Applying namespace: 34% complete +14:16:11.302 [debug] Building engine: +14:16:11.302 [debug] Building engine: Applying namespace: 34% complete +14:16:11.302 [debug] Building engine: +14:16:11.303 [debug] Building engine: Applying namespace: 34% complete +14:16:11.303 [debug] Building engine: +14:16:11.303 [debug] Building engine: Applying namespace: 34% complete +14:16:11.304 [debug] Building engine: +14:16:11.305 [debug] Building engine: Applying namespace: 34% complete +14:16:11.305 [debug] Building engine: +14:16:11.306 [debug] Building engine: Applying namespace: 35% complete +14:16:11.306 [debug] Building engine: +14:16:11.306 [debug] Building engine: Applying namespace: 35% complete +14:16:11.306 [debug] Building engine: +14:16:11.306 [debug] Building engine: Applying namespace: 35% complete +14:16:11.307 [debug] Building engine: +14:16:11.307 [debug] Building engine: Applying namespace: 35% complete +14:16:11.307 [debug] Building engine: +14:16:11.307 [debug] Building engine: Applying namespace: 35% complete +14:16:11.307 [debug] Building engine: Applying namespace: 35% complete +14:16:11.307 [debug] Building engine: Applying namespace: 35% complete +14:16:11.308 [debug] Building engine: +14:16:11.308 [debug] Building engine: Applying namespace: 35% complete +14:16:11.308 [debug] Building engine: +14:16:11.308 [debug] Building engine: Applying namespace: 35% complete +14:16:11.311 [debug] Building engine: Applying namespace: 35% complete +14:16:11.311 [debug] Building engine: Applying namespace: 35% complete +14:16:11.311 [debug] Building engine: +14:16:11.312 [debug] Building engine: Applying namespace: 35% complete +14:16:11.312 [debug] Building engine: +14:16:11.313 [debug] Building engine: Applying namespace: 35% complete +14:16:11.313 [debug] Building engine: +14:16:11.313 [debug] Building engine: Applying namespace: 35% complete +14:16:11.313 [debug] Building engine: +14:16:11.313 [debug] Building engine: Applying namespace: 35% complete +14:16:11.313 [debug] Building engine: +14:16:11.314 [debug] Building engine: Applying namespace: 35% complete +14:16:11.316 [debug] Building engine: +14:16:11.316 [debug] Building engine: Applying namespace: 36% complete +14:16:11.317 [debug] Building engine: +14:16:11.317 [debug] Building engine: Applying namespace: 36% complete +14:16:11.317 [debug] Building engine: +14:16:11.317 [debug] Building engine: Applying namespace: 36% complete +14:16:11.317 [debug] Building engine: +14:16:11.317 [debug] Building engine: Applying namespace: 36% complete +14:16:11.317 [debug] Building engine: +14:16:11.317 [debug] Building engine: Applying namespace: 36% complete +14:16:11.318 [debug] Building engine: +14:16:11.318 [debug] Building engine: Applying namespace: 36% complete +14:16:11.320 [debug] Building engine: +14:16:11.320 [debug] Building engine: Applying namespace: 36% complete +14:16:11.320 [debug] Building engine: +14:16:11.321 [debug] Building engine: Applying namespace: 36% complete +14:16:11.322 [debug] Building engine: +14:16:11.322 [debug] Building engine: Applying namespace: 36% complete +14:16:11.323 [debug] Building engine: +14:16:11.323 [debug] Building engine: Applying namespace: 36% complete +14:16:11.323 [debug] Building engine: +14:16:11.324 [debug] Building engine: Applying namespace: 36% complete +14:16:11.324 [debug] Building engine: +14:16:11.325 [debug] Building engine: Applying namespace: 36% complete +14:16:11.325 [debug] Building engine: +14:16:11.325 [debug] Building engine: Applying namespace: 36% complete +14:16:11.325 [debug] Building engine: +14:16:11.326 [debug] Building engine: Applying namespace: 36% complete +14:16:11.326 [debug] Building engine: +14:16:11.326 [debug] Building engine: Applying namespace: 36% complete +14:16:11.326 [debug] Building engine: +14:16:11.326 [debug] Building engine: Applying namespace: 36% complete +14:16:11.328 [debug] Building engine: Applying namespace: 36% complete +14:16:11.328 [debug] Building engine: +14:16:11.328 [debug] Building engine: Applying namespace: 37% complete +14:16:11.328 [debug] Building engine: +14:16:11.328 [debug] Building engine: Applying namespace: 37% complete +14:16:11.329 [debug] Building engine: +14:16:11.329 [debug] Building engine: Applying namespace: 37% complete +14:16:11.329 [debug] Building engine: +14:16:11.330 [debug] Building engine: Applying namespace: 37% complete +14:16:11.331 [debug] Building engine: +14:16:11.331 [debug] Building engine: Applying namespace: 37% complete +14:16:11.331 [debug] Building engine: +14:16:11.331 [debug] Building engine: Applying namespace: 37% complete +14:16:11.332 [debug] Building engine: +14:16:11.332 [debug] Building engine: Applying namespace: 37% complete +14:16:11.332 [debug] Building engine: +14:16:11.332 [debug] Building engine: Applying namespace: 37% complete +14:16:11.333 [debug] Building engine: +14:16:11.333 [debug] Building engine: Applying namespace: 37% complete +14:16:11.333 [debug] Building engine: +14:16:11.333 [debug] Building engine: Applying namespace: 37% complete +14:16:11.333 [debug] Building engine: +14:16:11.333 [debug] Building engine: Applying namespace: 37% complete +14:16:11.334 [debug] Building engine: +14:16:11.334 [debug] Building engine: Applying namespace: 37% complete +14:16:11.336 [debug] Building engine: +14:16:11.336 [debug] Building engine: Applying namespace: 37% complete +14:16:11.337 [debug] Building engine: +14:16:11.337 [debug] Building engine: Applying namespace: 37% complete +14:16:11.337 [debug] Building engine: +14:16:11.337 [debug] Building engine: Applying namespace: 37% complete +14:16:11.337 [debug] Building engine: +14:16:11.338 [debug] Building engine: Applying namespace: 37% complete +14:16:11.338 [debug] Building engine: +14:16:11.338 [debug] Building engine: Applying namespace: 38% complete +14:16:11.338 [debug] Building engine: Applying namespace: 38% complete +14:16:11.339 [debug] Building engine: +14:16:11.339 [debug] Building engine: Applying namespace: 38% complete +14:16:11.339 [debug] Building engine: +14:16:11.340 [debug] Building engine: Applying namespace: 38% complete +14:16:11.340 [debug] Building engine: +14:16:11.340 [debug] Building engine: Applying namespace: 38% complete +14:16:11.340 [debug] Building engine: +14:16:11.341 [debug] Building engine: Applying namespace: 38% complete +14:16:11.341 [debug] Building engine: +14:16:11.341 [debug] Building engine: Applying namespace: 38% complete +14:16:11.341 [debug] Building engine: +14:16:11.341 [debug] Building engine: Applying namespace: 38% complete +14:16:11.342 [debug] Building engine: +14:16:11.342 [debug] Building engine: Applying namespace: 38% complete +14:16:11.342 [debug] Building engine: +14:16:11.342 [debug] Building engine: Applying namespace: 38% complete +14:16:11.343 [debug] Building engine: Applying namespace: 38% complete Applying namespace: 38% complete +14:16:11.344 [debug] Building engine: +14:16:11.344 [debug] Building engine: Applying namespace: 38% complete +14:16:11.345 [debug] Building engine: Applying namespace: 38% complete +14:16:11.345 [debug] Building engine: +14:16:11.345 [debug] Building engine: Applying namespace: 38% complete +14:16:11.346 [debug] Building engine: +14:16:11.346 [debug] Building engine: Applying namespace: 38% complete +14:16:11.346 [debug] Building engine: +14:16:11.346 [debug] Building engine: Applying namespace: 39% complete +14:16:11.347 [debug] Building engine: Applying namespace: 39% complete Applying namespace: 39% complete +14:16:11.347 [debug] Building engine: +14:16:11.347 [debug] Building engine: Applying namespace: 39% complete +14:16:11.347 [debug] Building engine: +14:16:11.348 [debug] Building engine: Applying namespace: 39% complete +14:16:11.348 [debug] Building engine: +14:16:11.348 [debug] Building engine: Applying namespace: 39% complete +14:16:11.348 [debug] Building engine: +14:16:11.348 [debug] Building engine: Applying namespace: 39% complete +14:16:11.348 [debug] Building engine: +14:16:11.349 [debug] Building engine: Applying namespace: 39% complete +14:16:11.349 [debug] Building engine: +14:16:11.349 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 39% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 40% complete +14:16:11.367 [debug] Building engine: +14:16:11.367 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.376 [debug] Building engine: Applying namespace: 40% complete +14:16:11.376 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 40% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 40% complete +14:16:11.391 [debug] Building engine: Applying namespace: 40% complete Applying namespace: 40% complete Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: Applying namespace: 41% complete +14:16:11.391 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 41% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 41% complete +14:16:11.401 [debug] Building engine: Applying namespace: 41% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 41% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 41% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: +14:16:11.401 [debug] Building engine: Applying namespace: 42% complete +14:16:11.401 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: Applying namespace: 42% complete Applying namespace: 42% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 43% complete +14:16:11.407 [debug] Building engine: Applying namespace: 43% complete +14:16:11.407 [debug] Building engine: +14:16:11.407 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.417 [debug] Building engine: +14:16:11.417 [debug] Building engine: Applying namespace: 43% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 43% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 43% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 43% complete +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.426 [debug] Building engine: Applying namespace: 44% complete +14:16:11.426 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 44% complete +14:16:11.439 [debug] Building engine: +14:16:11.439 [debug] Building engine: Applying namespace: 45% complete +14:16:11.439 [debug] Building engine: +14:16:11.440 [debug] Building engine: Applying namespace: 45% complete +14:16:11.440 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.452 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.452 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.452 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.452 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.452 [debug] Building engine: +14:16:11.452 [debug] Building engine: Applying namespace: 45% complete +14:16:11.453 [debug] Building engine: +14:16:11.453 [debug] Building engine: Applying namespace: 45% complete +14:16:11.453 [debug] Building engine: +14:16:11.453 [debug] Building engine: Applying namespace: 45% complete +14:16:11.453 [debug] Building engine: +14:16:11.453 [debug] Building engine: Applying namespace: 45% complete +14:16:11.453 [debug] Building engine: +14:16:11.453 [debug] Building engine: Applying namespace: 45% complete +14:16:11.453 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 45% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 45% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 45% complete +14:16:11.469 [debug] Building engine: Applying namespace: 45% complete Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.469 [debug] Building engine: +14:16:11.469 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 46% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 47% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 47% complete +14:16:11.479 [debug] Building engine: +14:16:11.479 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.485 [debug] Building engine: Applying namespace: 47% complete +14:16:11.485 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 47% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 47% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 47% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 47% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.497 [debug] Building engine: Applying namespace: 48% complete Applying namespace: 48% complete +14:16:11.497 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 48% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 49% complete +14:16:11.503 [debug] Building engine: +14:16:11.503 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.511 [debug] Building engine: +14:16:11.511 [debug] Building engine: Applying namespace: 49% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 49% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 49% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 49% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 49% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.528 [debug] Building engine: +14:16:11.528 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.550 [debug] Building engine: Applying namespace: 50% complete +14:16:11.550 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 50% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.561 [debug] Building engine: Applying namespace: 51% complete +14:16:11.561 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.569 [debug] Building engine: Applying namespace: 51% complete +14:16:11.569 [debug] Building engine: +14:16:11.570 [debug] Building engine: Applying namespace: 51% complete +14:16:11.570 [debug] Building engine: +14:16:11.570 [debug] Building engine: Applying namespace: 52% complete +14:16:11.570 [debug] Building engine: +14:16:11.570 [debug] Building engine: Applying namespace: 52% complete +14:16:11.570 [debug] Building engine: +14:16:11.570 [debug] Building engine: Applying namespace: 52% complete +14:16:11.570 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.576 [debug] Building engine: +14:16:11.576 [debug] Building engine: Applying namespace: 52% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 52% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 52% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.587 [debug] Building engine: +14:16:11.587 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 53% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 54% complete +14:16:11.595 [debug] Building engine: +14:16:11.595 [debug] Building engine: Applying namespace: 54% complete +14:16:11.602 [debug] Building engine: +14:16:11.602 [debug] Building engine: Applying namespace: 54% complete +14:16:11.602 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.603 [debug] Building engine: +14:16:11.603 [debug] Building engine: Applying namespace: 54% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 54% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 54% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 54% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 54% complete +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.607 [debug] Building engine: Applying namespace: 55% complete +14:16:11.607 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.609 [debug] Building engine: Applying namespace: 55% complete +14:16:11.609 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.611 [debug] Building engine: Applying namespace: 56% complete +14:16:11.611 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 56% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 57% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 57% complete +14:16:11.614 [debug] Building engine: +14:16:11.614 [debug] Building engine: Applying namespace: 57% complete +14:16:11.614 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.617 [debug] Building engine: Applying namespace: 57% complete +14:16:11.617 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 57% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 57% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 57% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: Applying namespace: 58% complete +14:16:11.620 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 58% complete +14:16:11.623 [debug] Building engine: +14:16:11.623 [debug] Building engine: Applying namespace: 59% complete +14:16:11.623 [debug] Building engine: Applying namespace: 59% complete +14:16:11.623 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete Applying namespace: 59% complete Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 59% complete Applying namespace: 59% complete Applying namespace: 60% complete +14:16:11.627 [debug] Building engine: +14:16:11.627 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.628 [debug] Building engine: +14:16:11.628 [debug] Building engine: Applying namespace: 60% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 60% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 60% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 60% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.630 [debug] Building engine: +14:16:11.630 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 61% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 62% complete +14:16:11.631 [debug] Building engine: +14:16:11.631 [debug] Building engine: Applying namespace: 62% complete +14:16:11.631 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.632 [debug] Building engine: +14:16:11.632 [debug] Building engine: Applying namespace: 62% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 62% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 62% complete +14:16:11.634 [debug] Building engine: Applying namespace: 62% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete +14:16:11.634 [debug] Building engine: Applying namespace: 63% complete Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 63% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 64% complete +14:16:11.635 [debug] Building engine: Applying namespace: 64% complete +14:16:11.635 [debug] Building engine: +14:16:11.635 [debug] Building engine: Applying namespace: 64% complete +14:16:11.635 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete Applying namespace: 64% complete +14:16:11.637 [debug] Building engine: Applying namespace: 64% complete +14:16:11.638 [debug] Building engine: Applying namespace: 64% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.638 [debug] Building engine: +14:16:11.638 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: Applying namespace: 65% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 66% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 66% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 66% complete Applying namespace: 66% complete +14:16:11.640 [debug] Building engine: +14:16:11.640 [debug] Building engine: Applying namespace: 66% complete +14:16:11.640 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 66% complete +14:16:11.641 [debug] Building engine: +14:16:11.641 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.643 [debug] Building engine: Applying namespace: 67% complete +14:16:11.643 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 67% complete +14:16:11.645 [debug] Building engine: Applying namespace: 68% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 68% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 68% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 68% complete +14:16:11.645 [debug] Building engine: +14:16:11.645 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.646 [debug] Building engine: +14:16:11.646 [debug] Building engine: Applying namespace: 68% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 68% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.647 [debug] Building engine: +14:16:11.647 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 69% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 70% complete +14:16:11.649 [debug] Building engine: Applying namespace: 70% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 70% complete +14:16:11.649 [debug] Building engine: +14:16:11.649 [debug] Building engine: Applying namespace: 70% complete +14:16:11.649 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.650 [debug] Building engine: +14:16:11.650 [debug] Building engine: Applying namespace: 70% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 70% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.652 [debug] Building engine: Applying namespace: 71% complete +14:16:11.652 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 71% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 72% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 72% complete +14:16:11.653 [debug] Building engine: +14:16:11.653 [debug] Building engine: Applying namespace: 72% complete +14:16:11.653 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.655 [debug] Building engine: +14:16:11.655 [debug] Building engine: Applying namespace: 72% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 72% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 72% complete +14:16:11.656 [debug] Building engine: Applying namespace: 72% complete +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.656 [debug] Building engine: Applying namespace: 73% complete +14:16:11.656 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 73% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 73% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 73% complete +14:16:11.658 [debug] Building engine: Applying namespace: 73% complete Applying namespace: 73% complete Applying namespace: 73% complete +14:16:11.658 [debug] Building engine: Applying namespace: 73% complete Applying namespace: 73% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.658 [debug] Building engine: Applying namespace: 74% complete +14:16:11.658 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.659 [debug] Building engine: Applying namespace: 74% complete +14:16:11.659 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.660 [debug] Building engine: Applying namespace: 75% complete +14:16:11.660 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 75% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 76% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 76% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 76% complete +14:16:11.661 [debug] Building engine: +14:16:11.661 [debug] Building engine: Applying namespace: 76% complete Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.663 [debug] Building engine: +14:16:11.663 [debug] Building engine: Applying namespace: 76% complete +14:16:11.664 [debug] Building engine: Applying namespace: 76% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.664 [debug] Building engine: +14:16:11.664 [debug] Building engine: Applying namespace: 77% complete +14:16:11.666 [debug] Building engine: Applying namespace: 77% complete Applying namespace: 77% complete +14:16:11.666 [debug] Building engine: Applying namespace: 77% complete +14:16:11.666 [debug] Building engine: Applying namespace: 77% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 77% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.666 [debug] Building engine: Applying namespace: 78% complete +14:16:11.666 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 78% complete +14:16:11.667 [debug] Building engine: +14:16:11.667 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.668 [debug] Building engine: +14:16:11.668 [debug] Building engine: Applying namespace: 79% complete +14:16:11.669 [debug] Building engine: +14:16:11.669 [debug] Building engine: Applying namespace: 79% complete +14:16:11.669 [debug] Building engine: +14:16:11.669 [debug] Building engine: Applying namespace: 79% complete +14:16:11.669 [debug] Building engine: +14:16:11.669 [debug] Building engine: Applying namespace: 79% complete +14:16:11.669 [debug] Building engine: Applying namespace: 79% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 79% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 79% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 79% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 79% complete +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.670 [debug] Building engine: Applying namespace: 80% complete +14:16:11.670 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.671 [debug] Building engine: Applying namespace: 80% complete +14:16:11.671 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.673 [debug] Building engine: Applying namespace: 81% complete +14:16:11.673 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 81% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 82% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 82% complete +14:16:11.674 [debug] Building engine: +14:16:11.674 [debug] Building engine: Applying namespace: 82% complete +14:16:11.674 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.677 [debug] Building engine: +14:16:11.677 [debug] Building engine: Applying namespace: 82% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 82% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.679 [debug] Building engine: Applying namespace: 83% complete Applying namespace: 83% complete Applying namespace: 83% complete +14:16:11.679 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 83% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.681 [debug] Building engine: +14:16:11.681 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.682 [debug] Building engine: +14:16:11.682 [debug] Building engine: Applying namespace: 84% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.684 [debug] Building engine: +14:16:11.684 [debug] Building engine: Applying namespace: 85% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 85% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 85% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 85% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 85% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.685 [debug] Building engine: +14:16:11.685 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.687 [debug] Building engine: +14:16:11.687 [debug] Building engine: Applying namespace: 86% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.688 [debug] Building engine: Applying namespace: 87% complete +14:16:11.688 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 87% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 87% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 87% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 87% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.690 [debug] Building engine: +14:16:11.690 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 88% complete +14:16:11.691 [debug] Building engine: Applying namespace: 89% complete +14:16:11.691 [debug] Building engine: +14:16:11.691 [debug] Building engine: Applying namespace: 89% complete +14:16:11.691 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.693 [debug] Building engine: Applying namespace: 89% complete +14:16:11.693 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 89% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 89% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 89% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 89% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 89% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.694 [debug] Building engine: +14:16:11.694 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 90% complete +14:16:11.695 [debug] Building engine: +14:16:11.695 [debug] Building engine: Applying namespace: 91% complete +14:16:11.695 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete +14:16:11.697 [debug] Building engine: Applying namespace: 91% complete Applying namespace: 91% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 91% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 91% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 91% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.698 [debug] Building engine: +14:16:11.698 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 92% complete +14:16:11.699 [debug] Building engine: +14:16:11.699 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.700 [debug] Building engine: Applying namespace: 93% complete +14:16:11.700 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 93% complete +14:16:11.702 [debug] Building engine: Applying namespace: 93% complete +14:16:11.702 [debug] Building engine: Applying namespace: 93% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 93% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 93% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.702 [debug] Building engine: Applying namespace: 94% complete +14:16:11.702 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.703 [debug] Building engine: +14:16:11.703 [debug] Building engine: Applying namespace: 94% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.704 [debug] Building engine: +14:16:11.704 [debug] Building engine: Applying namespace: 95% complete +14:16:11.711 [debug] Building engine: +14:16:11.711 [debug] Building engine: Applying namespace: 95% complete +14:16:11.714 [debug] Building engine: +14:16:11.714 [debug] Building engine: Applying namespace: 95% complete +14:16:11.728 [debug] Building engine: +14:16:11.728 [debug] Building engine: Applying namespace: 95% complete +14:16:11.728 [debug] Building engine: +14:16:11.729 [debug] Building engine: Applying namespace: 95% complete +14:16:11.730 [debug] Building engine: +14:16:11.730 [debug] Building engine: Applying namespace: 95% complete +14:16:11.731 [debug] Building engine: +14:16:11.731 [debug] Building engine: Applying namespace: 95% complete +14:16:11.732 [debug] Building engine: +14:16:11.732 [debug] Building engine: Applying namespace: 95% complete +14:16:11.734 [debug] Building engine: +14:16:11.734 [debug] Building engine: Applying namespace: 96% complete +14:16:11.741 [debug] Building engine: +14:16:11.742 [debug] Building engine: Applying namespace: 96% complete +14:16:11.742 [debug] Building engine: +14:16:11.742 [debug] Building engine: Applying namespace: 96% complete +14:16:11.744 [debug] Building engine: +14:16:11.744 [debug] Building engine: Applying namespace: 96% complete +14:16:11.746 [debug] Building engine: +14:16:11.746 [debug] Building engine: Applying namespace: 96% complete +14:16:11.753 [debug] Building engine: +14:16:11.754 [debug] Building engine: Applying namespace: 96% complete +14:16:11.756 [debug] Building engine: +14:16:11.756 [debug] Building engine: Applying namespace: 96% complete +14:16:11.756 [debug] Building engine: +14:16:11.756 [debug] Building engine: Applying namespace: 96% complete +14:16:11.757 [debug] Building engine: +14:16:11.757 [debug] Building engine: Applying namespace: 96% complete +14:16:11.760 [debug] Building engine: +14:16:11.760 [debug] Building engine: Applying namespace: 96% complete +14:16:11.767 [debug] Building engine: +14:16:11.767 [debug] Building engine: Applying namespace: 96% complete +14:16:11.767 [debug] Building engine: +14:16:11.768 [debug] Building engine: Applying namespace: 96% complete +14:16:11.776 [debug] Building engine: +14:16:11.776 [debug] Building engine: Applying namespace: 96% complete +14:16:11.779 [debug] Building engine: +14:16:11.779 [debug] Building engine: Applying namespace: 96% complete +14:16:11.780 [debug] Building engine: +14:16:11.780 [debug] Building engine: Applying namespace: 96% complete +14:16:11.781 [debug] Building engine: +14:16:11.781 [debug] Building engine: Applying namespace: 96% complete +14:16:11.781 [debug] Building engine: +14:16:11.782 [debug] Building engine: Applying namespace: 97% complete +14:16:11.782 [debug] Building engine: +14:16:11.785 [debug] Building engine: Applying namespace: 97% complete +14:16:11.786 [debug] Building engine: +14:16:11.786 [debug] Building engine: Applying namespace: 97% complete +14:16:11.794 [debug] Building engine: +14:16:11.795 [debug] Building engine: Applying namespace: 97% complete +14:16:11.796 [debug] Building engine: +14:16:11.796 [debug] Building engine: Applying namespace: 97% complete +14:16:11.797 [debug] Building engine: +14:16:11.797 [debug] Building engine: Applying namespace: 97% complete +14:16:11.797 [debug] Building engine: +14:16:11.797 [debug] Building engine: Applying namespace: 97% complete +14:16:11.797 [debug] Building engine: +14:16:11.798 [debug] Building engine: Applying namespace: 97% complete +14:16:11.799 [debug] Building engine: +14:16:11.800 [debug] Building engine: Applying namespace: 97% complete +14:16:11.805 [debug] Building engine: Applying namespace: 97% complete +14:16:11.806 [debug] Building engine: Applying namespace: 97% complete +14:16:11.806 [debug] Building engine: +14:16:11.806 [debug] Building engine: Applying namespace: 97% complete +14:16:11.808 [debug] Building engine: +14:16:11.808 [debug] Building engine: Applying namespace: 97% complete +14:16:11.813 [debug] Building engine: +14:16:11.815 [debug] Building engine: Applying namespace: 97% complete +14:16:11.815 [debug] Building engine: +14:16:11.816 [debug] Building engine: Applying namespace: 97% complete +14:16:11.817 [debug] Building engine: +14:16:11.818 [debug] Building engine: Applying namespace: 97% complete +14:16:11.820 [debug] Building engine: Applying namespace: 97% complete +14:16:11.820 [debug] Building engine: +14:16:11.820 [debug] Building engine: Applying namespace: 98% complete +14:16:11.821 [debug] Building engine: +14:16:11.822 [debug] Building engine: Applying namespace: 98% complete +14:16:11.822 [debug] Building engine: +14:16:11.823 [debug] Building engine: Applying namespace: 98% complete +14:16:11.823 [debug] Building engine: +14:16:11.824 [debug] Building engine: Applying namespace: 98% complete +14:16:11.824 [debug] Building engine: +14:16:11.824 [debug] Building engine: Applying namespace: 98% complete +14:16:11.825 [debug] Building engine: +14:16:11.825 [debug] Building engine: Applying namespace: 98% complete +14:16:11.825 [debug] Building engine: +14:16:11.825 [debug] Building engine: Applying namespace: 98% complete +14:16:11.826 [debug] Building engine: +14:16:11.827 [debug] Building engine: Applying namespace: 98% complete +14:16:11.828 [debug] Building engine: +14:16:11.828 [debug] Building engine: Applying namespace: 98% complete +14:16:11.832 [debug] Building engine: +14:16:11.832 [debug] Building engine: Applying namespace: 98% complete +14:16:11.832 [debug] Building engine: +14:16:11.832 [debug] Building engine: Applying namespace: 98% complete +14:16:11.833 [debug] Building engine: +14:16:11.833 [debug] Building engine: Applying namespace: 98% complete +14:16:11.835 [debug] Building engine: +14:16:11.835 [debug] Building engine: Applying namespace: 98% complete +14:16:11.840 [debug] Building engine: +14:16:11.840 [debug] Building engine: Applying namespace: 98% complete +14:16:11.841 [debug] Building engine: +14:16:11.841 [debug] Building engine: Applying namespace: 98% complete +14:16:11.855 [debug] Building engine: +14:16:11.855 [debug] Building engine: Applying namespace: 98% complete +14:16:11.865 [debug] Building engine: +14:16:11.867 [debug] Building engine: Applying namespace: 99% complete +14:16:11.868 [debug] Building engine: +14:16:11.870 [debug] Building engine: Applying namespace: 99% complete +14:16:11.880 [debug] Building engine: +14:16:11.880 [debug] Building engine: Applying namespace: 99% complete +14:16:11.880 [debug] Building engine: +14:16:11.880 [debug] Building engine: Applying namespace: 99% complete +14:16:11.882 [debug] Building engine: +14:16:11.882 [debug] Building engine: Applying namespace: 99% complete +14:16:11.883 [debug] Building engine: +14:16:11.883 [debug] Building engine: Applying namespace: 99% complete +14:16:11.885 [debug] Building engine: +14:16:11.886 [debug] Building engine: Applying namespace: 99% complete +14:16:11.887 [debug] Building engine: +14:16:11.887 [debug] Building engine: Applying namespace: 99% complete +14:16:11.888 [debug] Building engine: +14:16:11.888 [debug] Building engine: Applying namespace: 99% complete +14:16:11.891 [debug] Building engine: +14:16:11.892 [debug] Building engine: Applying namespace: 99% complete +14:16:11.898 [debug] Building engine: +14:16:11.898 [debug] Building engine: Applying namespace: 99% complete +14:16:11.899 [debug] Building engine: +14:16:11.899 [debug] Building engine: Applying namespace: 99% complete +14:16:11.900 [debug] Building engine: +14:16:11.900 [debug] Building engine: Applying namespace: 99% complete +14:16:11.900 [debug] Building engine: +14:16:11.900 [debug] Building engine: Applying namespace: 99% complete +14:16:11.901 [debug] Building engine: +14:16:11.901 [debug] Building engine: Applying namespace: 99% complete +14:16:11.907 [debug] Building engine: +14:16:11.907 [debug] Building engine: Applying namespace: 99% complete +14:16:11.909 [debug] Building engine: +14:16:11.909 [debug] Building engine: Applying namespace: 100% complete +14:16:11.941 [debug] Building engine: +14:16:11.941 [debug] Building engine: Applying namespace: 100% complete +14:16:11.946 [debug] Building engine: +14:16:11.946 [debug] Building engine: Applying namespace: 100% complete +14:16:11.957 [debug] Building engine: +14:16:11.958 [debug] Building engine: Applying namespace: 100% complete +14:16:11.985 [debug] Building engine: +14:16:11.985 [debug] Building engine: Applying namespace: 100% complete +14:16:12.070 [debug] Building engine: +14:16:12.071 [debug] Building engine: Applying namespace: 100% complete +14:16:12.159 [debug] Building engine: +14:16:12.159 [debug] Building engine: Applying namespace: 100% complete +14:16:12.286 [debug] Building engine: +14:16:12.286 [debug] Building engine: Applying namespace: 100% complete +14:16:13.299 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/error.ex +14:16:13.299 [debug] handled notification client -> server textDocument/didOpen in 456µs +14:16:13.696 [debug] Building engine: +14:16:13.696 [debug] Building engine: Applying namespace: 100% complete +14:16:13.696 [debug] Building engine: + done + +14:16:13.700 [debug] Building engine: Rewriting 0 scripts + +14:16:13.701 [debug] Building engine: Rebuilding 0 boot files + +14:16:13.722 [debug] Building engine: Rewriting 0 config scripts. + +14:16:13.727 [info] Engine build available at: /Users/james.cleveland/Library/Application Support/Expert/0.1.0-0ff78f1/ex-1.19.3-erl-16.1.2/eg4WdUvInV_ZD842qW3usA/_build/dev_ns +14:16:13.750 [debug] sent notification server -> client $/progress +14:16:14.277 [debug] Node port message: ok + +14:16:14.298 [debug] sent notification server -> client $/progress +14:16:14.298 [error] Process #PID<0.173.0> terminating +** (exit) {:badrpc, {:EXIT, {%SyntaxError{file: "/Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", line: 4, column: 1, snippet: "<<<<<<< HEAD", description: "found an unexpected version control marker, please resolve the conflicts: <<<<<<< HEAD"}, [{:elixir_compiler, :string, 3, [file: ~c"src/elixir_compiler.erl", line: 11]}, {Module.ParallelChecker, :verify, 1, [file: ~c"lib/module/parallel_checker.ex", line: 155]}, {XPEngine.Bootstrap, :maybe_load_mix_exs, 1, [file: ~c"lib/engine/bootstrap.ex", line: 103]}, {XPEngine.Bootstrap, :init, 3, [file: ~c"lib/engine/bootstrap.ex", line: 27]}]}}} + (stdlib 6.2.2.1) gen_server.erl:2210: :gen_server.init_it/6 + (stdlib 6.2.2.1) proc_lib.erl:329: :proc_lib.init_p_do_apply/3 +Initial Call: XPExpert.Project.Node.init/1 +Ancestors: [:"grpc::supervisor", XPExpert.ProjectSupervisor, XPExpert.Supervisor, #PID<0.157.0>] +Message Queue Length: 0 +Messages: [] +Links: [#PID<0.170.0>] +Dictionary: [] +Trapping Exits: false +Status: :running +Heap Size: 75113 +Stack Size: 29 +Reductions: 33300 +14:16:14.299 [error] Child {XPExpert.Project.Node, "grpc"} of Supervisor :"grpc::supervisor" failed to start +** (exit) {:badrpc, {:EXIT, {%SyntaxError{file: "/Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", line: 4, column: 1, snippet: "<<<<<<< HEAD", description: "found an unexpected version control marker, please resolve the conflicts: <<<<<<< HEAD"}, [{:elixir_compiler, :string, 3, [file: ~c"src/elixir_compiler.erl", line: 11]}, {Module.ParallelChecker, :verify, 1, [file: ~c"lib/module/parallel_checker.ex", line: 155]}, {XPEngine.Bootstrap, :maybe_load_mix_exs, 1, [file: ~c"lib/engine/bootstrap.ex", line: 103]}, {XPEngine.Bootstrap, :init, 3, [file: ~c"lib/engine/bootstrap.ex", line: 27]}]}}} +Start Call: XPExpert.Project.Node.start_link(%XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}) +Restart: :permanent +Shutdown: 5000 +Type: :worker +14:16:14.300 [error] Task #PID<0.169.0> started from #PID<0.168.0> terminating +** (MatchError) no match of right hand side value: {:error, {:shutdown, {:failed_to_start_child, {XPExpert.Project.Node, "grpc"}, {:badrpc, {:EXIT, {%SyntaxError{file: "/Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", line: 4, column: 1, snippet: "<<<<<<< HEAD", description: "found an unexpected version control marker, please resolve the conflicts: <<<<<<< HEAD"}, [{:elixir_compiler, :string, 3, [file: ~c"src/elixir_compiler.erl", line: 11]}, {Module.ParallelChecker, :verify, 1, [file: ~c"lib/module/parallel_checker.ex", line: 155]}, {XPEngine.Bootstrap, :maybe_load_mix_exs, 1, [file: ~c"lib/engine/bootstrap.ex", line: 103]}, {XPEngine.Bootstrap, :init, 3, [file: ~c"lib/engine/bootstrap.ex", line: 27]}]}}}}}} + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:59: anonymous fn/1 in XPExpert.State.initialize/2 + (elixir 1.17.3) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2 +Function: #Function<3.104070401/0 in XPExpert.State.initialize/2> + Args: [] +14:16:14.301 [error] Child :undefined of Supervisor :expert_task_queue terminated +** (exit) an exception was raised: + ** (MatchError) no match of right hand side value: {:error, {:shutdown, {:failed_to_start_child, {XPExpert.Project.Node, "grpc"}, {:badrpc, {:EXIT, {%SyntaxError{file: "/Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", line: 4, column: 1, snippet: "<<<<<<< HEAD", description: "found an unexpected version control marker, please resolve the conflicts: <<<<<<< HEAD"}, [{:elixir_compiler, :string, 3, [file: ~c"src/elixir_compiler.erl", line: 11]}, {Module.ParallelChecker, :verify, 1, [file: ~c"lib/module/parallel_checker.ex", line: 155]}, {XPEngine.Bootstrap, :maybe_load_mix_exs, 1, [file: ~c"lib/engine/bootstrap.ex", line: 103]}, {XPEngine.Bootstrap, :init, 3, [file: ~c"lib/engine/bootstrap.ex", line: 27]}]}}}}}} + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:59: anonymous fn/1 in XPExpert.State.initialize/2 + (elixir 1.17.3) lib/task/supervised.ex:101: Task.Supervised.invoke_mfa/2 +Pid: #PID<0.169.0> +Start Call: Task.Supervised.start_link/? +Restart: :temporary +Shutdown: 5000 +Type: :worker +14:16:52.388 [info] Child of Supervisor :inet_gethost_native_sup started +Pid: #PID<0.195.0> +Start Call: :inet_gethost_native.init([]) +14:16:52.388 [debug] Child :inet_gethost_native_sup of Supervisor :kernel_safe_sup started +Pid: #PID<0.194.0> +Start Call: :inet_gethost_native.start_link() +Restart: :temporary +Shutdown: 1000 +Type: :worker +14:16:52.400 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:52.400 [debug] sent notification server -> client window/logMessage +14:16:52.400 [warning] client -> server notification crashed +14:16:52.401 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:52.401 [debug] sent notification server -> client window/logMessage +14:16:52.401 [warning] client -> server notification crashed +14:16:52.430 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:52.430 [debug] sent notification server -> client window/logMessage +14:16:52.430 [warning] client -> server notification crashed +14:16:52.456 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:52.456 [debug] sent notification server -> client window/logMessage +14:16:52.456 [warning] client -> server notification crashed +14:16:52.457 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:52.457 [debug] sent notification server -> client window/logMessage +14:16:52.457 [warning] client -> server notification crashed +14:16:52.467 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:52.467 [debug] sent notification server -> client window/logMessage +14:16:52.467 [warning] client -> server notification crashed +14:16:52.489 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:52.489 [debug] sent notification server -> client window/logMessage +14:16:52.489 [warning] client -> server notification crashed +14:16:52.491 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:52.491 [debug] sent notification server -> client window/logMessage +14:16:52.491 [warning] client -> server notification crashed +14:16:58.879 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:58.879 [debug] sent notification server -> client window/logMessage +14:16:58.879 [warning] client -> server notification crashed +14:16:58.892 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:58.892 [debug] sent notification server -> client window/logMessage +14:16:58.892 [warning] client -> server notification crashed +14:16:58.981 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:58.981 [debug] sent notification server -> client window/logMessage +14:16:58.981 [warning] client -> server notification crashed +14:16:58.998 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:16:58.998 [debug] sent notification server -> client window/logMessage +14:16:58.998 [warning] client -> server notification crashed +14:16:59.007 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:59.007 [debug] sent notification server -> client window/logMessage +14:16:59.007 [warning] client -> server notification crashed +14:16:59.007 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:59.007 [debug] sent notification server -> client window/logMessage +14:16:59.007 [warning] client -> server notification crashed +14:16:59.008 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:59.008 [debug] sent notification server -> client window/logMessage +14:16:59.008 [warning] client -> server notification crashed +14:16:59.029 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:16:59.029 [debug] sent notification server -> client window/logMessage +14:16:59.029 [warning] client -> server notification crashed +14:17:10.655 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:10.655 [debug] sent notification server -> client window/logMessage +14:17:10.655 [warning] client -> server notification crashed +14:17:10.655 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:10.655 [debug] sent notification server -> client window/logMessage +14:17:10.655 [warning] client -> server notification crashed +14:17:10.666 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:10.666 [debug] sent notification server -> client window/logMessage +14:17:10.666 [warning] client -> server notification crashed +14:17:10.750 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:10.750 [debug] sent notification server -> client window/logMessage +14:17:10.750 [warning] client -> server notification crashed +14:17:10.763 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:10.763 [debug] sent notification server -> client window/logMessage +14:17:10.763 [warning] client -> server notification crashed +14:17:10.774 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:10.774 [debug] sent notification server -> client window/logMessage +14:17:10.774 [warning] client -> server notification crashed +14:17:10.774 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:10.774 [debug] sent notification server -> client window/logMessage +14:17:10.774 [warning] client -> server notification crashed +14:17:10.775 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:10.775 [debug] sent notification server -> client window/logMessage +14:17:10.775 [warning] client -> server notification crashed +14:17:10.794 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:10.794 [debug] sent notification server -> client window/logMessage +14:17:10.794 [warning] client -> server notification crashed +14:17:19.423 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:19.423 [debug] sent notification server -> client window/logMessage +14:17:19.423 [warning] client -> server notification crashed +14:17:19.424 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:19.424 [debug] sent notification server -> client window/logMessage +14:17:19.424 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:19.424 [debug] sent notification server -> client window/logMessage +14:17:19.424 [warning] client -> server notification crashed +14:17:19.424 [warning] client -> server notification crashed +14:17:19.528 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:19.528 [debug] sent notification server -> client window/logMessage +14:17:19.528 [warning] client -> server notification crashed +14:17:19.529 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:19.529 [debug] sent notification server -> client window/logMessage +14:17:19.529 [warning] client -> server notification crashed +14:17:19.529 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:19.529 [debug] sent notification server -> client window/logMessage +14:17:19.529 [warning] client -> server notification crashed +14:17:19.535 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:17:19.535 [debug] sent notification server -> client window/logMessage +14:17:19.535 [warning] client -> server notification crashed +14:17:19.544 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:19.544 [debug] sent notification server -> client window/logMessage +14:17:19.544 [warning] client -> server notification crashed +14:17:19.566 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:17:19.566 [debug] sent notification server -> client window/logMessage +14:17:19.566 [warning] client -> server notification crashed +14:20:19.656 [error] Save failed for uri file:///Users/james.cleveland/workspace/surgeventures/grpc/.github/workflows/ci.yml error was {:error, :not_open} +14:20:19.656 [error] Failed to handle textDocument/didSave, {{:error, :not_open}, %XPExpert.State{configuration: %XPExpert.Configuration{project: %XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}, support: %XPExpert.Configuration.Support{code_action_dynamic_registration: true, hierarchical_symbols: true, snippet: true, deprecated: true, tags: %{value_set: [1]}, signature_help: %XPGenLSP.Structures.SignatureHelpClientCapabilities{context_support: nil, signature_information: %{documentation_format: ["markdown", "plaintext"], parameter_information: %{label_offset_support: true}, active_parameter_support: true}, dynamic_registration: true}, work_done_progress: true}, client_name: "Zed", additional_watched_extensions: nil, dialyzer_enabled?: false}, initialized?: true, engine_initialized?: false, shutdown_received?: false, in_flight_requests: %{}}} +14:20:19.656 [debug] handled notification client -> server textDocument/didSave in 5ms +14:20:45.790 [error] Save failed for uri file:///Users/james.cleveland/workspace/surgeventures/grpc/README.md error was {:error, :not_open} +14:20:45.790 [error] Failed to handle textDocument/didSave, {{:error, :not_open}, %XPExpert.State{configuration: %XPExpert.Configuration{project: %XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}, support: %XPExpert.Configuration.Support{code_action_dynamic_registration: true, hierarchical_symbols: true, snippet: true, deprecated: true, tags: %{value_set: [1]}, signature_help: %XPGenLSP.Structures.SignatureHelpClientCapabilities{context_support: nil, signature_information: %{documentation_format: ["markdown", "plaintext"], parameter_information: %{label_offset_support: true}, active_parameter_support: true}, dynamic_registration: true}, work_done_progress: true}, client_name: "Zed", additional_watched_extensions: nil, dialyzer_enabled?: false}, initialized?: true, engine_initialized?: false, shutdown_received?: false, in_flight_requests: %{}}} +14:20:45.790 [debug] handled notification client -> server textDocument/didSave in 331µs +14:21:06.969 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:06.969 [debug] sent notification server -> client window/logMessage +14:21:06.969 [warning] client -> server notification crashed +14:21:07.381 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:07.381 [debug] sent notification server -> client window/logMessage +14:21:07.381 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:21:07.381 [debug] sent notification server -> client window/logMessage +14:21:07.381 [warning] client -> server notification crashed +14:21:07.381 [warning] client -> server notification crashed +14:21:12.788 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:21:12.788 [debug] sent notification server -> client window/logMessage +14:21:12.788 [warning] client -> server notification crashed +14:21:18.289 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:21:18.289 [debug] sent notification server -> client window/logMessage +14:21:18.289 [warning] client -> server notification crashed +14:21:18.291 [debug] handled notification client -> server textDocument/didClose in 2ms +14:21:32.147 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/client/adapters/gun.ex +14:21:32.147 [debug] handled notification client -> server textDocument/didOpen in 809µs +14:21:44.475 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:21:44.475 [debug] sent notification server -> client window/logMessage +14:21:44.475 [warning] client -> server notification crashed +14:21:44.494 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:44.494 [debug] sent notification server -> client window/logMessage +14:21:44.494 [warning] client -> server notification crashed +14:21:44.518 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:44.518 [debug] sent notification server -> client window/logMessage +14:21:44.518 [warning] client -> server notification crashed +14:21:44.570 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:21:44.570 [debug] sent notification server -> client window/logMessage +14:21:44.570 [warning] client -> server notification crashed +14:21:44.589 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:44.589 [debug] sent notification server -> client window/logMessage +14:21:44.589 [warning] client -> server notification crashed +14:21:44.590 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:21:44.590 [debug] sent notification server -> client window/logMessage +14:21:44.590 [warning] client -> server notification crashed +14:22:05.503 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:22:05.503 [debug] sent notification server -> client window/logMessage +14:22:05.503 [warning] client -> server notification crashed +14:22:05.505 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:22:05.505 [debug] sent notification server -> client window/logMessage +14:22:05.505 [warning] client -> server notification crashed +14:22:05.509 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:22:05.509 [debug] sent notification server -> client window/logMessage +14:22:05.509 [warning] client -> server notification crashed +14:22:11.218 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:22:11.218 [debug] sent notification server -> client window/logMessage +14:22:11.218 [warning] client -> server notification crashed +14:22:11.269 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:22:11.270 [debug] sent notification server -> client window/logMessage +14:22:11.270 [warning] client -> server notification crashed +14:22:11.334 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:22:11.334 [debug] sent notification server -> client window/logMessage +14:22:11.334 [warning] client -> server notification crashed +14:22:27.666 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:22:27.666 [debug] sent notification server -> client window/logMessage +14:22:27.666 [warning] client -> server notification crashed +14:22:27.968 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:22:27.968 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:22:27.968 [debug] sent notification server -> client window/logMessage +14:22:27.968 [debug] sent notification server -> client window/logMessage +14:22:27.968 [warning] client -> server notification crashed +14:22:27.968 [warning] client -> server notification crashed +14:23:25.557 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:23:25.557 [debug] sent notification server -> client window/logMessage +14:23:25.557 [warning] client -> server notification crashed +14:23:35.565 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/grpc/transport/utils.ex +14:23:35.565 [debug] handled notification client -> server textDocument/didOpen in 566µs +14:23:40.422 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:23:40.423 [debug] sent notification server -> client window/logMessage +14:23:40.423 [warning] client -> server notification crashed +14:23:40.831 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:23:40.831 [debug] sent notification server -> client window/logMessage +14:23:40.831 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:23:40.831 [debug] sent notification server -> client window/logMessage +14:23:40.831 [warning] client -> server notification crashed +14:23:40.831 [warning] client -> server notification crashed +14:23:45.270 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:23:45.270 [debug] sent notification server -> client window/logMessage +14:23:45.270 [warning] client -> server notification crashed +14:23:45.564 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:23:45.564 [debug] sent notification server -> client window/logMessage +14:23:45.564 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:23:45.564 [debug] sent notification server -> client window/logMessage +14:23:45.564 [warning] client -> server notification crashed +14:23:45.564 [warning] client -> server notification crashed +14:24:00.122 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/google/rpc/status.pb.ex +14:24:00.122 [debug] handled notification client -> server textDocument/didOpen in 294µs +14:24:06.517 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:24:06.517 [debug] sent notification server -> client window/logMessage +14:24:06.517 [warning] client -> server notification crashed +14:24:06.854 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:24:06.854 [debug] sent notification server -> client window/logMessage +14:24:06.854 [warning] client -> server notification crashed +14:24:06.854 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:24:06.854 [debug] sent notification server -> client window/logMessage +14:24:06.854 [warning] client -> server notification crashed +14:24:16.953 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/lib/google/rpc/error_details.pb.ex +14:24:16.953 [debug] handled notification client -> server textDocument/didOpen in 588µs +14:24:44.043 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:24:44.043 [debug] sent notification server -> client window/logMessage +14:24:44.043 [warning] client -> server notification crashed +14:24:44.043 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:24:44.043 [debug] sent notification server -> client window/logMessage +14:24:44.043 [warning] client -> server notification crashed +14:24:56.755 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:24:56.755 [debug] sent notification server -> client window/logMessage +14:24:56.755 [warning] client -> server notification crashed +14:24:57.045 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:24:57.045 [debug] sent notification server -> client window/logMessage +14:24:57.045 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:24:57.045 [warning] client -> server notification crashed +14:24:57.045 [debug] sent notification server -> client window/logMessage +14:24:57.045 [warning] client -> server notification crashed +14:26:10.641 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/test/grpc/transport/http2_test.exs +14:26:10.641 [debug] handled notification client -> server textDocument/didOpen in 2ms +14:26:20.186 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:20.186 [debug] sent notification server -> client window/logMessage +14:26:20.186 [warning] client -> server notification crashed +14:26:20.617 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:26:20.617 [debug] sent notification server -> client window/logMessage +14:26:20.617 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:20.617 [debug] sent notification server -> client window/logMessage +14:26:20.617 [warning] client -> server notification crashed +14:26:20.617 [warning] client -> server notification crashed +14:26:27.725 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:27.725 [debug] sent notification server -> client window/logMessage +14:26:27.725 [warning] client -> server notification crashed +14:26:28.034 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:26:28.034 [debug] sent notification server -> client window/logMessage +14:26:28.034 [warning] client -> server notification crashed +14:26:28.034 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:28.034 [debug] sent notification server -> client window/logMessage +14:26:28.034 [warning] client -> server notification crashed +14:26:32.685 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/test/grpc/integration/compressor_test.exs +14:26:32.685 [debug] handled notification client -> server textDocument/didOpen in 613µs +14:26:38.266 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:38.266 [debug] sent notification server -> client window/logMessage +14:26:38.266 [warning] client -> server notification crashed +14:26:38.630 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:26:38.630 [debug] sent notification server -> client window/logMessage +14:26:38.630 [warning] client -> server notification crashed +14:26:38.630 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:26:38.630 [debug] sent notification server -> client window/logMessage +14:26:38.630 [warning] client -> server notification crashed +14:27:04.270 [info] Opened file:///Users/james.cleveland/workspace/surgeventures/grpc/test/grpc/transport/utils_test.exs +14:27:04.270 [debug] handled notification client -> server textDocument/didOpen in 1ms +14:27:10.435 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:10.435 [debug] sent notification server -> client window/logMessage +14:27:10.435 [warning] client -> server notification crashed +14:27:10.804 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:10.804 [debug] sent notification server -> client window/logMessage +14:27:10.804 [warning] client -> server notification crashed +14:27:10.804 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:27:10.804 [debug] sent notification server -> client window/logMessage +14:27:10.804 [warning] client -> server notification crashed +14:27:10.805 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:10.805 [debug] sent notification server -> client window/logMessage +14:27:10.805 [warning] client -> server notification crashed +14:27:17.021 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:17.021 [debug] sent notification server -> client window/logMessage +14:27:17.021 [warning] client -> server notification crashed +14:27:17.304 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:122: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:17.304 [debug] sent notification server -> client window/logMessage +14:27:17.304 [warning] client -> server notification crashed +14:27:17.304 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (elixir 1.17.3) lib/enum.ex:987: Enum."-each/2-lists^foreach/1-0-"/2 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:198: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + +14:27:17.304 [debug] sent notification server -> client window/logMessage +14:27:17.304 [warning] client -> server notification crashed +14:27:17.304 [error] ** (ErlangError) Erlang error: {:erpc, :noconnection} + (kernel 10.2.7.1) erpc.erl:1376: :erpc.call/5 + (xp_expert 0.1.0-0ff78f1) lib/expert/state.ex:173: XPExpert.State.apply/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:205: XPExpert.apply_to_state/2 + (xp_expert 0.1.0-0ff78f1) lib/expert.ex:144: XPExpert.handle_notification/2 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:519: anonymous fn/2 in XPGenLSP.loop/3 + (xp_telemetry 1.3.0) /home/runner/work/expert/expert/apps/expert/deps/telemetry/src/telemetry.erl:324: :xp_telemetry.span/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:515: anonymous fn/4 in XPGenLSP.loop/3 + (xp_gen_lsp 0.11.1) lib/gen_lsp.ex:592: anonymous fn/4 in XPGenLSP.attempt/4 + +14:27:17.304 [debug] sent notification server -> client window/logMessage +14:27:17.304 [warning] client -> server notification crashed +14:28:41.954 [debug] handled notification client -> server workspace/didChangeConfiguration in 735µs +14:28:41.954 [debug] handled notification client -> server workspace/didChangeConfiguration in 94µs +14:28:45.276 [error] Save failed for uri file:///Users/james.cleveland/workspace/surgeventures/grpc/.gitignore error was {:error, :not_open} +14:28:45.277 [error] Failed to handle textDocument/didSave, {{:error, :not_open}, %XPExpert.State{configuration: %XPExpert.Configuration{project: %XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}, support: %XPExpert.Configuration.Support{code_action_dynamic_registration: true, hierarchical_symbols: true, snippet: true, deprecated: true, tags: %{value_set: [1]}, signature_help: %XPGenLSP.Structures.SignatureHelpClientCapabilities{context_support: nil, signature_information: %{documentation_format: ["markdown", "plaintext"], parameter_information: %{label_offset_support: true}, active_parameter_support: true}, dynamic_registration: true}, work_done_progress: true}, client_name: "Zed", additional_watched_extensions: nil, dialyzer_enabled?: false}, initialized?: true, engine_initialized?: false, shutdown_received?: false, in_flight_requests: %{}}} +14:28:45.277 [debug] handled notification client -> server textDocument/didSave in 473µs +14:28:51.441 [error] Save failed for uri file:///Users/james.cleveland/workspace/surgeventures/grpc/.gitignore error was {:error, :not_open} +14:28:51.441 [error] Failed to handle textDocument/didSave, {{:error, :not_open}, %XPExpert.State{configuration: %XPExpert.Configuration{project: %XPForge.Project{root_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc", mix_exs_uri: "file:///Users/james.cleveland/workspace/surgeventures/grpc/mix.exs", mix_project?: true, mix_env: nil, mix_target: nil, env_variables: %{}, project_module: nil, entropy: 62787}, support: %XPExpert.Configuration.Support{code_action_dynamic_registration: true, hierarchical_symbols: true, snippet: true, deprecated: true, tags: %{value_set: [1]}, signature_help: %XPGenLSP.Structures.SignatureHelpClientCapabilities{context_support: nil, signature_information: %{documentation_format: ["markdown", "plaintext"], parameter_information: %{label_offset_support: true}, active_parameter_support: true}, dynamic_registration: true}, work_done_progress: true}, client_name: "Zed", additional_watched_extensions: nil, dialyzer_enabled?: false}, initialized?: true, engine_initialized?: false, shutdown_received?: false, in_flight_requests: %{}}} +14:28:51.441 [debug] handled notification client -> server textDocument/didSave in 407µs diff --git a/.formatter.exs b/.formatter.exs index 26b1f1456..6ee672804 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,6 +1,9 @@ - [ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}", "examples/*/{config,lib,priv}/*.ex"], + inputs: [ + "{mix,.formatter}.exs", + "{config,lib,test}/**/*.{ex,exs}", + "examples/*/{config,lib,priv}/*.ex" + ], import_deps: [:protobuf], locals_without_parens: [rpc: 3, intercept: 1, intercept: 2, run: 1, run: 2], export: [ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8939532be..68d44b771 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,12 +3,12 @@ name: Elixir CI Checks env: DEBIAN_FRONTEND: noninteractive DEPENDENCY_FILE: mix.lock - ELIXIR_VERSION: 1.10.4 # Elixir version used during package publishing + ELIXIR_VERSION: 1.16.0 JOB_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - OTP_VERSION: 22.3.4.7 # OTP version used during package publishing - RELEVANT_FILES: "benchmark config examples interop lib priv src test mix.exs mix.lock" # Important, this controls the caching, make sure to keep this right + OTP_VERSION: 26.1 + RELEVANT_FILES: "benchmark config examples interop lib priv src test mix.exs mix.lock" REPOSITORY: grpc - RUNNER_OS: ubuntu20 # Must match Elixir/OTP version in described in action erlef/setup-beam@v1 + RUNNER_OS: ubuntu22 SHA: ${{ github.sha }} concurrency: @@ -34,9 +34,9 @@ jobs: strategy: fail-fast: false matrix: - otp: [21.3.8.24, 22.3.4.7] - elixir: [1.10.4] - runner-os: [ubuntu20] + otp: [26.1] + elixir: [1.16.0] + runner-os: [ubuntu22] steps: - name: Checkout latest codebase uses: actions/checkout@v4 @@ -80,7 +80,7 @@ jobs: - uses: runs-on/cache@v4 id: build-cache with: - path: '**/*' + path: "**/*" key: ${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-compile-dev-${{ steps.hash.outputs.HASH }} - name: Compile with warning as --warnings-as-errors if: steps.build-cache.outputs.cache-hit != 'true' @@ -97,15 +97,16 @@ jobs: run: | echo "Running publish --dry-run" mix hex.publish --dry-run + test: name: Unit Tests (Elixir ${{ matrix.elixir }} & OTP ${{ matrix.otp }}) runs-on: runs-on,runner=2cpu-linux-x64 strategy: fail-fast: false matrix: - otp: [21.3.8.24, 22.3.4.7] - elixir: [1.10.4] - runner-os: [ubuntu20] + otp: [26.1] + elixir: [1.16.0] + runner-os: [ubuntu22] steps: - name: Checkout latest codebase uses: actions/checkout@v4 @@ -149,7 +150,7 @@ jobs: - uses: runs-on/cache@v4 id: build-cache with: - path: '**/*' + path: "**/*" key: ${{ runner.os }}-${{ matrix.elixir }}-${{ matrix.otp }}-compile-test-${{ steps.hash.outputs.HASH }} - name: Compile with MIX_ENV=test if: steps.build-cache.outputs.cache-hit != 'true' @@ -162,6 +163,7 @@ jobs: run: | echo "Running tests" mix test --cover + permit: name: Permit Package Publishing needs: [static, test] @@ -248,31 +250,36 @@ jobs: echo "Publishing package" mix hex.publish --yes - interop-tests: runs-on: runs-on,runner=2cpu-linux-x64 name: Interop tests - container: - image: elixir:1.10-slim + needs: [static] steps: - - uses: actions/checkout@v4 - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - working-directory: ./interop - - name: Run interop tests - run: mix run script/run.exs - working-directory: ./interop + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: 26.1 + elixir-version: 1.16.0 + - name: Install Dependencies + run: | + mix local.rebar --force + mix local.hex --force + mix deps.get + working-directory: ./interop + - name: Run interop tests + run: mix run script/run.exs + working-directory: ./interop check_release: runs-on: runs-on,runner=2cpu-linux-x64 name: Check release - container: - image: elixir:1.10-slim + needs: [static] steps: - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: 26.1 + elixir-version: 1.16.0 - name: Install Dependencies run: | mix local.rebar --force diff --git a/.github/workflows/cron_ci.yml b/.github/workflows/cron_ci.yml deleted file mode 100644 index beebc9a42..000000000 --- a/.github/workflows/cron_ci.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Cron CI - -on: - schedule: - - cron: '0 0 * * 0' - -jobs: - interop-tests: - runs-on: ubuntu-latest - name: Interop tests - container: - image: elixir:1.9 - steps: - - uses: actions/checkout@v1 - - name: Install Dependencies - run: | - mix local.rebar --force - mix local.hex --force - mix deps.get - working-directory: ./interop - - name: Run Cron CI - run: make ci-cron diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 000000000..510eaf42f --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +elixir 1.15.7-otp-26 +erlang 26.1.2 diff --git a/MIGRATION_0.6_to_0.10.md b/MIGRATION_0.6_to_0.10.md new file mode 100644 index 000000000..5a25ebd90 --- /dev/null +++ b/MIGRATION_0.6_to_0.10.md @@ -0,0 +1,99 @@ +# Migration Guide: grpc_fresha 0.6 → 0.10 + +## Requirements + +- **Elixir**: 1.15+ (was 1.5+) +- **OTP**: 25+ (was 21+) +- **protobuf**: ~> 0.14 (was ~> 0.10) + +## Breaking Changes + +### Module Renames + +```elixir +# Error module +GRPC.Error → GRPC.RPCError + +# Client adapters +GRPC.Adapter.Gun → GRPC.Client.Adapters.Gun + +# Server adapters +GRPC.Adapter.Cowboy → GRPC.Server.Adapters.Cowboy +``` + +### RPC Response Tuples + +```elixir +# Success - unchanged +{:ok, response} + +# Error - now 2-tuple instead of 3-tuple +# Before (0.6): +{:error, %GRPC.RPCError{}, _extra} + +# After (0.10): +{:error, %GRPC.RPCError{}} +``` + +### Protobuf Struct Construction + +protobuf 0.14 deprecates `.new/1` - use struct syntax: + +```elixir +# Before: +Google.Rpc.Status.new(code: 3, message: "error") + +# After: +%Google.Rpc.Status{code: 3, message: "error"} +``` + +### HTTP2 Server Trailers + +```elixir +# Before: +GRPC.Transport.HTTP2.server_trailers(status, message, custom_trailers) + +# After (3rd arg removed): +GRPC.Transport.HTTP2.server_trailers(status, message) +``` + +### Base64 Binary Metadata + +Binary metadata (`*-bin` headers) now uses standard base64 with padding: + +```elixir +# Before: "woA" +# After: "woA=" +``` + +## New Features (Non-Breaking) + +- **HTTP Transcoding**: REST/JSON gateway for gRPC services +- **CORS Support**: Cross-origin request handling +- **Mint Adapter**: Alternative HTTP/2 client (`GRPC.Client.Adapters.Mint`) +- **Flow Dependency**: For stream processing +- **Telemetry**: Built-in instrumentation events + +## Quick Migration Checklist + +1. Update `mix.exs` deps: + ```elixir + {:grpc_fresha, "~> 0.10"} + {:protobuf, "~> 0.14"} + ``` + +2. Find/replace module names: + - `GRPC.Error` → `GRPC.RPCError` + - `GRPC.Adapter.Gun` → `GRPC.Client.Adapters.Gun` + - `GRPC.Adapter.Cowboy` → `GRPC.Server.Adapters.Cowboy` + +3. Update error handling patterns from 3-tuple to 2-tuple + +4. Replace `.new()` calls with `%Struct{}` syntax + +5. Regenerate protobuf files: + ```bash + mix protobuf.generate --include-path=./priv/protos ./priv/protos/*.proto + ``` + +6. Run tests to catch remaining issues diff --git a/Makefile b/Makefile index efc78c469..33479e25d 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,4 @@ test-all: mix test cd interop && mix run script/run.exs -# This is heavy -ci-cron: - cd interop && mix deps.get && mix run script/run.exs --rounds 1000 --concurrency 30 && cd - - mix deps.get && bash .ci/build-plt-cache.sh && mix dialyzer - - .PHONY: test release test-prepare test-all ci-cron diff --git a/README.md b/README.md index 084b2afb1..7093a1bea 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,50 @@ # gRPC Elixir -[![Hex.pm](https://img.shields.io/hexpm/v/grpc.svg)](https://hex.pm/packages/grpc) -[![Travis Status](https://travis-ci.org/elixir-grpc/grpc.svg?branch=master)](https://travis-ci.org/elixir-grpc/grpc) -[![GitHub actions Status](https://github.com/elixir-grpc/grpc/workflows/CI/badge.svg)](https://github.com/elixir-grpc/grpc/actions) -[![Inline docs](http://inch-ci.org/github/elixir-grpc/grpc.svg?branch=master)](http://inch-ci.org/github/elixir-grpc/grpc) +[![GitHub CI](https://github.com/surgeventures/grpc/actions/workflows/ci.yml/badge.svg)](https://github.com/surgeventures/grpc/actions/workflows/ci.yml) -An Elixir implementation of [gRPC](http://www.grpc.io/). +Fresha fork of [elixir-grpc/grpc](https://github.com/elixir-grpc/grpc) - an Elixir implementation of [gRPC](http://www.grpc.io/). -**WARNING: Be careful to use it in production! Test and benchmark in advance.** - -**NOTICE: Erlang/OTP needs >= 20.3.2** +**Requires: Elixir >= 1.15, OTP >= 24** ## Installation -The package can be installed as: - ```elixir def deps do [ - {:grpc, "~> 0.6", hex: :grpc_fresha} + {:grpc, "~> 0.10", hex: :grpc_fresha} ] end ``` ## Usage -1. Generate Elixir code from proto file as [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) shows(especially the `gRPC Support` section). -2. Implement the server side code like below and remember to return the expected message types. +1. Write your protobuf file: + +```protobuf +syntax = "proto3"; + +package helloworld; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) {} +} +``` + +2. Generate Elixir code from proto file using [protobuf-elixir](https://github.com/elixir-protobuf/protobuf#usage): + +```shell +protoc --elixir_out=plugins=grpc:./lib -I./priv/protos helloworld.proto +``` + +3. Implement the server: ```elixir defmodule Helloworld.Greeter.Server do @@ -39,96 +57,102 @@ defmodule Helloworld.Greeter.Server do end ``` -3. Start the server - -You can start the gRPC server as a supervised process. First, add `GRPC.Server.Supervisor` to your supervision tree. +4. Define endpoint: ```elixir -# Define your endpoint defmodule Helloworld.Endpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Server.Interceptors.Logger run Helloworld.Greeter.Server end +``` + +5. Start server in your supervision tree: -# In the start function of your Application +```elixir defmodule HelloworldApp do use Application + def start(_type, _args) do children = [ - # ... - supervisor(GRPC.Server.Supervisor, [{Helloworld.Endpoint, 50051}]) + {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true} ] - - opts = [strategy: :one_for_one, name: YourApp] - Supervisor.start_link(children, opts) + Supervisor.start_link(children, strategy: :one_for_one) end end ``` -Then start it when starting your application: +6. Call RPC: ```elixir -# config.exs -config :grpc, start_server: true +{:ok, channel} = GRPC.Stub.connect("localhost:50051") +request = Helloworld.HelloRequest.new(name: "grpc-elixir") +{:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(request) -# test.exs -config :grpc, start_server: false - -$ iex -S mix +# With interceptors +{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger]) ``` -or run grpc.server using a mix task +## Client Adapters -``` -$ mix grpc.server +Default adapter is `GRPC.Client.Adapters.Gun`. You can use Mint instead: + +```elixir +GRPC.Stub.connect("localhost:50051", adapter: GRPC.Client.Adapters.Mint) ``` -4. Call rpc: +Configure Mint adapter options: ```elixir -iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051") -iex> request = Helloworld.HelloRequest.new(name: "grpc-elixir") -iex> {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(request) +config :grpc, GRPC.Client.Adapters.Mint, + timeout: 10_000, + transport_opts: [cacertfile: "/path/to/ca.crt"] +``` -# With interceptors -iex> {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) -... +## HTTP Transcoding + +Enable transcoding for HTTP/JSON access to gRPC methods: + +```elixir +defmodule Helloworld.Greeter.Server do + use GRPC.Server, + service: Helloworld.Greeter.Service, + http_transcode: true + # ... +end ``` -Check [examples](examples) and [interop](interop)(Interoperability Test) for some examples. +See [examples/helloworld_transcoding](examples/helloworld_transcoding) for full example. -## TODO +## CORS -- [x] Unary RPC -- [x] Server streaming RPC -- [x] Client streaming RPC -- [x] Bidirectional streaming RPC -- [x] Helloworld and RouteGuide examples -- [x] Doc and more tests -- [x] Authentication with TLS -- [x] Improve code generation from protos ([protobuf-elixir](https://github.com/tony612/protobuf-elixir) [#8](https://github.com/elixir-grpc/grpc/issues/8)) -- [x] Timeout for unary calls -- [x] Errors handling -- [x] Benchmarking -- [x] Logging -- [x] Interceptors(See `GRPC.Endpoint`) -- [x] [Connection Backoff](https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md) -- [x] Data compression -- [x] Support other encoding(other than protobuf) +Add CORS headers for browser access: -## Benchmark +```elixir +defmodule Helloworld.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Server.Interceptors.Logger + intercept GRPC.Server.Interceptors.CORS, allow_origin: "mydomain.io" + run Helloworld.Greeter.Server +end +``` -1. [Simple benchmark](examples/helloworld/README.md#Benchmark) by using [ghz](https://ghz.sh/) +## Features -2. [Benchmark](benchmark) followed by official spec +- Unary, server-streaming, client-streaming, bidirectional-streaming RPC +- HTTP Transcoding +- TLS Authentication +- Interceptors +- Connection Backoff +- Data compression +- gRPC Reflection (via [grpc-reflection](https://github.com/elixir-grpc/grpc-reflection)) -## Contributing +## Migration -You contributions are welcome! +See [MIGRATION.md](MIGRATION.md) for upgrade guides between versions. + +## Contributing -Please open issues if you have questions, problems and ideas. You can create pull -requests directly if you want to fix little bugs, add small features and so on. -But you'd better use issues first if you want to add a big feature or change a -lot of code. +See upstream [elixir-grpc/grpc](https://github.com/elixir-grpc/grpc) for issues and contributions. diff --git a/benchmark/README.md b/benchmark/README.md index 6936cab22..46f53db9b 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -5,7 +5,7 @@ Benchmark implementation followed by [official spec](https://grpc.io/docs/guides ## Usage ``` -$ git clone https://github.com/tony612/grpc.git +$ git clone https://github.com/elixir-grpc/grpc.git $ cd grpc && git checkout -t origin/elixir-bench $ git submodule update --init $ export ELIXIR_GRPC_PATH= # elixir-grpc path diff --git a/benchmark/config/config.exs b/benchmark/config/config.exs index db5eb576e..1c0128596 100644 --- a/benchmark/config/config.exs +++ b/benchmark/config/config.exs @@ -1,4 +1,4 @@ -use Mix.Config +import Config config :logger, level: :info diff --git a/benchmark/config/dev.exs b/benchmark/config/dev.exs index d2d855e6d..becde7693 100644 --- a/benchmark/config/dev.exs +++ b/benchmark/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/benchmark/config/prod.exs b/benchmark/config/prod.exs index 28c45b137..63787d4b3 100644 --- a/benchmark/config/prod.exs +++ b/benchmark/config/prod.exs @@ -1,3 +1,3 @@ -use Mix.Config +import Config -config :logger, level: :warn +config :logger, level: :warning diff --git a/benchmark/lib/grpc/core/stats.pb.ex b/benchmark/lib/grpc/core/stats.pb.ex index fcb7f196b..d63472627 100644 --- a/benchmark/lib/grpc/core/stats.pb.ex +++ b/benchmark/lib/grpc/core/stats.pb.ex @@ -1,12 +1,7 @@ defmodule Grpc.Core.Bucket do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - start: float, - count: non_neg_integer - } - defstruct [:start, :count] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :start, 1, type: :double field :count, 2, type: :uint64 @@ -14,27 +9,19 @@ end defmodule Grpc.Core.Histogram do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - buckets: [Grpc.Core.Bucket.t()] - } - defstruct [:buckets] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :buckets, 1, repeated: true, type: Grpc.Core.Bucket end defmodule Grpc.Core.Metric do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - value: {atom, any}, - name: String.t() - } - defstruct [:value, :name] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :value, 0 + field :name, 1, type: :string field :count, 10, type: :uint64, oneof: 0 field :histogram, 11, type: Grpc.Core.Histogram, oneof: 0 @@ -42,12 +29,8 @@ end defmodule Grpc.Core.Stats do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - metrics: [Grpc.Core.Metric.t()] - } - defstruct [:metrics] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :metrics, 1, repeated: true, type: Grpc.Core.Metric end diff --git a/benchmark/lib/grpc/testing/benchmark_service.pb.ex b/benchmark/lib/grpc/testing/benchmark_service.pb.ex index 1fde72c56..8b1378917 100644 --- a/benchmark/lib/grpc/testing/benchmark_service.pb.ex +++ b/benchmark/lib/grpc/testing/benchmark_service.pb.ex @@ -1,15 +1 @@ -defmodule Grpc.Testing.BenchmarkService.Service do - @moduledoc false - use GRPC.Service, name: "grpc.testing.BenchmarkService" - rpc :UnaryCall, Grpc.Testing.SimpleRequest, Grpc.Testing.SimpleResponse - rpc :StreamingCall, stream(Grpc.Testing.SimpleRequest), stream(Grpc.Testing.SimpleResponse) - rpc :StreamingFromClient, stream(Grpc.Testing.SimpleRequest), Grpc.Testing.SimpleResponse - rpc :StreamingFromServer, Grpc.Testing.SimpleRequest, stream(Grpc.Testing.SimpleResponse) - rpc :StreamingBothWays, stream(Grpc.Testing.SimpleRequest), stream(Grpc.Testing.SimpleResponse) -end - -defmodule Grpc.Testing.BenchmarkService.Stub do - @moduledoc false - use GRPC.Stub, service: Grpc.Testing.BenchmarkService.Service -end diff --git a/benchmark/lib/grpc/testing/control.pb.ex b/benchmark/lib/grpc/testing/control.pb.ex index 6b73ab913..9687a0f78 100644 --- a/benchmark/lib/grpc/testing/control.pb.ex +++ b/benchmark/lib/grpc/testing/control.pb.ex @@ -1,237 +1,171 @@ +defmodule Grpc.Testing.ClientType do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :SYNC_CLIENT, 0 + field :ASYNC_CLIENT, 1 + field :OTHER_CLIENT, 2 +end + +defmodule Grpc.Testing.ServerType do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :SYNC_SERVER, 0 + field :ASYNC_SERVER, 1 + field :ASYNC_GENERIC_SERVER, 2 + field :OTHER_SERVER, 3 +end + +defmodule Grpc.Testing.RpcType do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :UNARY, 0 + field :STREAMING, 1 + field :STREAMING_FROM_CLIENT, 2 + field :STREAMING_FROM_SERVER, 3 + field :STREAMING_BOTH_WAYS, 4 +end + defmodule Grpc.Testing.PoissonParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - offered_load: float - } - defstruct [:offered_load] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :offered_load, 1, type: :double + field :offered_load, 1, type: :double, json_name: "offeredLoad" end defmodule Grpc.Testing.ClosedLoopParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 end defmodule Grpc.Testing.LoadParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - load: {atom, any} - } - defstruct [:load] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :load, 0 - field :closed_loop, 1, type: Grpc.Testing.ClosedLoopParams, oneof: 0 + + field :closed_loop, 1, type: Grpc.Testing.ClosedLoopParams, json_name: "closedLoop", oneof: 0 field :poisson, 2, type: Grpc.Testing.PoissonParams, oneof: 0 end defmodule Grpc.Testing.SecurityParams do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - use_test_ca: boolean, - server_host_override: String.t(), - cred_type: String.t() - } - defstruct [:use_test_ca, :server_host_override, :cred_type] - - field :use_test_ca, 1, type: :bool - field :server_host_override, 2, type: :string - field :cred_type, 3, type: :string + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :use_test_ca, 1, type: :bool, json_name: "useTestCa" + field :server_host_override, 2, type: :string, json_name: "serverHostOverride" + field :cred_type, 3, type: :string, json_name: "credType" end defmodule Grpc.Testing.ChannelArg do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - value: {atom, any}, - name: String.t() - } - defstruct [:value, :name] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :value, 0 + field :name, 1, type: :string - field :str_value, 2, type: :string, oneof: 0 - field :int_value, 3, type: :int32, oneof: 0 + field :str_value, 2, type: :string, json_name: "strValue", oneof: 0 + field :int_value, 3, type: :int32, json_name: "intValue", oneof: 0 end defmodule Grpc.Testing.ClientConfig do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - server_targets: [String.t()], - client_type: atom | integer, - security_params: Grpc.Testing.SecurityParams.t() | nil, - outstanding_rpcs_per_channel: integer, - client_channels: integer, - async_client_threads: integer, - rpc_type: atom | integer, - load_params: Grpc.Testing.LoadParams.t() | nil, - payload_config: Grpc.Testing.PayloadConfig.t() | nil, - histogram_params: Grpc.Testing.HistogramParams.t() | nil, - core_list: [integer], - core_limit: integer, - other_client_api: String.t(), - channel_args: [Grpc.Testing.ChannelArg.t()], - threads_per_cq: integer, - messages_per_stream: integer, - use_coalesce_api: boolean - } - defstruct [ - :server_targets, - :client_type, - :security_params, - :outstanding_rpcs_per_channel, - :client_channels, - :async_client_threads, - :rpc_type, - :load_params, - :payload_config, - :histogram_params, - :core_list, - :core_limit, - :other_client_api, - :channel_args, - :threads_per_cq, - :messages_per_stream, - :use_coalesce_api - ] - - field :server_targets, 1, repeated: true, type: :string - field :client_type, 2, type: Grpc.Testing.ClientType, enum: true - field :security_params, 3, type: Grpc.Testing.SecurityParams - field :outstanding_rpcs_per_channel, 4, type: :int32 - field :client_channels, 5, type: :int32 - field :async_client_threads, 7, type: :int32 - field :rpc_type, 8, type: Grpc.Testing.RpcType, enum: true - field :load_params, 10, type: Grpc.Testing.LoadParams - field :payload_config, 11, type: Grpc.Testing.PayloadConfig - field :histogram_params, 12, type: Grpc.Testing.HistogramParams - field :core_list, 13, repeated: true, type: :int32 - field :core_limit, 14, type: :int32 - field :other_client_api, 15, type: :string - field :channel_args, 16, repeated: true, type: Grpc.Testing.ChannelArg - field :threads_per_cq, 17, type: :int32 - field :messages_per_stream, 18, type: :int32 - field :use_coalesce_api, 19, type: :bool + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :server_targets, 1, repeated: true, type: :string, json_name: "serverTargets" + field :client_type, 2, type: Grpc.Testing.ClientType, json_name: "clientType", enum: true + field :security_params, 3, type: Grpc.Testing.SecurityParams, json_name: "securityParams" + field :outstanding_rpcs_per_channel, 4, type: :int32, json_name: "outstandingRpcsPerChannel" + field :client_channels, 5, type: :int32, json_name: "clientChannels" + field :async_client_threads, 7, type: :int32, json_name: "asyncClientThreads" + field :rpc_type, 8, type: Grpc.Testing.RpcType, json_name: "rpcType", enum: true + field :load_params, 10, type: Grpc.Testing.LoadParams, json_name: "loadParams" + field :payload_config, 11, type: Grpc.Testing.PayloadConfig, json_name: "payloadConfig" + field :histogram_params, 12, type: Grpc.Testing.HistogramParams, json_name: "histogramParams" + field :core_list, 13, repeated: true, type: :int32, json_name: "coreList" + field :core_limit, 14, type: :int32, json_name: "coreLimit" + field :other_client_api, 15, type: :string, json_name: "otherClientApi" + field :channel_args, 16, repeated: true, type: Grpc.Testing.ChannelArg, json_name: "channelArgs" + field :threads_per_cq, 17, type: :int32, json_name: "threadsPerCq" + field :messages_per_stream, 18, type: :int32, json_name: "messagesPerStream" + field :use_coalesce_api, 19, type: :bool, json_name: "useCoalesceApi" end defmodule Grpc.Testing.ClientStatus do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - stats: Grpc.Testing.ClientStats.t() | nil - } - defstruct [:stats] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :stats, 1, type: Grpc.Testing.ClientStats end defmodule Grpc.Testing.Mark do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - reset: boolean - } - defstruct [:reset] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :reset, 1, type: :bool end defmodule Grpc.Testing.ClientArgs do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - argtype: {atom, any} - } - defstruct [:argtype] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :argtype, 0 + field :setup, 1, type: Grpc.Testing.ClientConfig, oneof: 0 field :mark, 2, type: Grpc.Testing.Mark, oneof: 0 end defmodule Grpc.Testing.ServerConfig do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - server_type: atom | integer, - security_params: Grpc.Testing.SecurityParams.t() | nil, - port: integer, - async_server_threads: integer, - core_limit: integer, - payload_config: Grpc.Testing.PayloadConfig.t() | nil, - core_list: [integer], - other_server_api: String.t(), - threads_per_cq: integer, - resource_quota_size: integer, - channel_args: [Grpc.Testing.ChannelArg.t()] - } - defstruct [ - :server_type, - :security_params, - :port, - :async_server_threads, - :core_limit, - :payload_config, - :core_list, - :other_server_api, - :threads_per_cq, - :resource_quota_size, - :channel_args - ] - - field :server_type, 1, type: Grpc.Testing.ServerType, enum: true - field :security_params, 2, type: Grpc.Testing.SecurityParams + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :server_type, 1, type: Grpc.Testing.ServerType, json_name: "serverType", enum: true + field :security_params, 2, type: Grpc.Testing.SecurityParams, json_name: "securityParams" field :port, 4, type: :int32 - field :async_server_threads, 7, type: :int32 - field :core_limit, 8, type: :int32 - field :payload_config, 9, type: Grpc.Testing.PayloadConfig - field :core_list, 10, repeated: true, type: :int32 - field :other_server_api, 11, type: :string - field :threads_per_cq, 12, type: :int32 - field :resource_quota_size, 1001, type: :int32 - field :channel_args, 1002, repeated: true, type: Grpc.Testing.ChannelArg + field :async_server_threads, 7, type: :int32, json_name: "asyncServerThreads" + field :core_limit, 8, type: :int32, json_name: "coreLimit" + field :payload_config, 9, type: Grpc.Testing.PayloadConfig, json_name: "payloadConfig" + field :core_list, 10, repeated: true, type: :int32, json_name: "coreList" + field :other_server_api, 11, type: :string, json_name: "otherServerApi" + field :threads_per_cq, 12, type: :int32, json_name: "threadsPerCq" + field :resource_quota_size, 1001, type: :int32, json_name: "resourceQuotaSize" + + field :channel_args, 1002, + repeated: true, + type: Grpc.Testing.ChannelArg, + json_name: "channelArgs" end defmodule Grpc.Testing.ServerArgs do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - argtype: {atom, any} - } - defstruct [:argtype] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :argtype, 0 + field :setup, 1, type: Grpc.Testing.ServerConfig, oneof: 0 field :mark, 2, type: Grpc.Testing.Mark, oneof: 0 end defmodule Grpc.Testing.ServerStatus do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - stats: Grpc.Testing.ServerStats.t() | nil, - port: integer, - cores: integer - } - defstruct [:stats, :port, :cores] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :stats, 1, type: Grpc.Testing.ServerStats field :port, 2, type: :int32 @@ -240,208 +174,92 @@ end defmodule Grpc.Testing.CoreRequest do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 end defmodule Grpc.Testing.CoreResponse do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - cores: integer - } - defstruct [:cores] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :cores, 1, type: :int32 end defmodule Grpc.Testing.Void do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 end defmodule Grpc.Testing.Scenario do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - name: String.t(), - client_config: Grpc.Testing.ClientConfig.t() | nil, - num_clients: integer, - server_config: Grpc.Testing.ServerConfig.t() | nil, - num_servers: integer, - warmup_seconds: integer, - benchmark_seconds: integer, - spawn_local_worker_count: integer - } - defstruct [ - :name, - :client_config, - :num_clients, - :server_config, - :num_servers, - :warmup_seconds, - :benchmark_seconds, - :spawn_local_worker_count - ] + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :name, 1, type: :string - field :client_config, 2, type: Grpc.Testing.ClientConfig - field :num_clients, 3, type: :int32 - field :server_config, 4, type: Grpc.Testing.ServerConfig - field :num_servers, 5, type: :int32 - field :warmup_seconds, 6, type: :int32 - field :benchmark_seconds, 7, type: :int32 - field :spawn_local_worker_count, 8, type: :int32 + field :client_config, 2, type: Grpc.Testing.ClientConfig, json_name: "clientConfig" + field :num_clients, 3, type: :int32, json_name: "numClients" + field :server_config, 4, type: Grpc.Testing.ServerConfig, json_name: "serverConfig" + field :num_servers, 5, type: :int32, json_name: "numServers" + field :warmup_seconds, 6, type: :int32, json_name: "warmupSeconds" + field :benchmark_seconds, 7, type: :int32, json_name: "benchmarkSeconds" + field :spawn_local_worker_count, 8, type: :int32, json_name: "spawnLocalWorkerCount" end defmodule Grpc.Testing.Scenarios do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - scenarios: [Grpc.Testing.Scenario.t()] - } - defstruct [:scenarios] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :scenarios, 1, repeated: true, type: Grpc.Testing.Scenario end defmodule Grpc.Testing.ScenarioResultSummary do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - qps: float, - qps_per_server_core: float, - server_system_time: float, - server_user_time: float, - client_system_time: float, - client_user_time: float, - latency_50: float, - latency_90: float, - latency_95: float, - latency_99: float, - latency_999: float, - server_cpu_usage: float, - successful_requests_per_second: float, - failed_requests_per_second: float, - client_polls_per_request: float, - server_polls_per_request: float, - server_queries_per_cpu_sec: float, - client_queries_per_cpu_sec: float - } - defstruct [ - :qps, - :qps_per_server_core, - :server_system_time, - :server_user_time, - :client_system_time, - :client_user_time, - :latency_50, - :latency_90, - :latency_95, - :latency_99, - :latency_999, - :server_cpu_usage, - :successful_requests_per_second, - :failed_requests_per_second, - :client_polls_per_request, - :server_polls_per_request, - :server_queries_per_cpu_sec, - :client_queries_per_cpu_sec - ] + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :qps, 1, type: :double - field :qps_per_server_core, 2, type: :double - field :server_system_time, 3, type: :double - field :server_user_time, 4, type: :double - field :client_system_time, 5, type: :double - field :client_user_time, 6, type: :double - field :latency_50, 7, type: :double - field :latency_90, 8, type: :double - field :latency_95, 9, type: :double - field :latency_99, 10, type: :double - field :latency_999, 11, type: :double - field :server_cpu_usage, 12, type: :double - field :successful_requests_per_second, 13, type: :double - field :failed_requests_per_second, 14, type: :double - field :client_polls_per_request, 15, type: :double - field :server_polls_per_request, 16, type: :double - field :server_queries_per_cpu_sec, 17, type: :double - field :client_queries_per_cpu_sec, 18, type: :double + field :qps_per_server_core, 2, type: :double, json_name: "qpsPerServerCore" + field :server_system_time, 3, type: :double, json_name: "serverSystemTime" + field :server_user_time, 4, type: :double, json_name: "serverUserTime" + field :client_system_time, 5, type: :double, json_name: "clientSystemTime" + field :client_user_time, 6, type: :double, json_name: "clientUserTime" + field :latency_50, 7, type: :double, json_name: "latency50" + field :latency_90, 8, type: :double, json_name: "latency90" + field :latency_95, 9, type: :double, json_name: "latency95" + field :latency_99, 10, type: :double, json_name: "latency99" + field :latency_999, 11, type: :double, json_name: "latency999" + field :server_cpu_usage, 12, type: :double, json_name: "serverCpuUsage" + + field :successful_requests_per_second, 13, + type: :double, + json_name: "successfulRequestsPerSecond" + + field :failed_requests_per_second, 14, type: :double, json_name: "failedRequestsPerSecond" + field :client_polls_per_request, 15, type: :double, json_name: "clientPollsPerRequest" + field :server_polls_per_request, 16, type: :double, json_name: "serverPollsPerRequest" + field :server_queries_per_cpu_sec, 17, type: :double, json_name: "serverQueriesPerCpuSec" + field :client_queries_per_cpu_sec, 18, type: :double, json_name: "clientQueriesPerCpuSec" end defmodule Grpc.Testing.ScenarioResult do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - scenario: Grpc.Testing.Scenario.t() | nil, - latencies: Grpc.Testing.HistogramData.t() | nil, - client_stats: [Grpc.Testing.ClientStats.t()], - server_stats: [Grpc.Testing.ServerStats.t()], - server_cores: [integer], - summary: Grpc.Testing.ScenarioResultSummary.t() | nil, - client_success: [boolean], - server_success: [boolean], - request_results: [Grpc.Testing.RequestResultCount.t()] - } - defstruct [ - :scenario, - :latencies, - :client_stats, - :server_stats, - :server_cores, - :summary, - :client_success, - :server_success, - :request_results - ] + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :scenario, 1, type: Grpc.Testing.Scenario field :latencies, 2, type: Grpc.Testing.HistogramData - field :client_stats, 3, repeated: true, type: Grpc.Testing.ClientStats - field :server_stats, 4, repeated: true, type: Grpc.Testing.ServerStats - field :server_cores, 5, repeated: true, type: :int32 + field :client_stats, 3, repeated: true, type: Grpc.Testing.ClientStats, json_name: "clientStats" + field :server_stats, 4, repeated: true, type: Grpc.Testing.ServerStats, json_name: "serverStats" + field :server_cores, 5, repeated: true, type: :int32, json_name: "serverCores" field :summary, 6, type: Grpc.Testing.ScenarioResultSummary - field :client_success, 7, repeated: true, type: :bool - field :server_success, 8, repeated: true, type: :bool - field :request_results, 9, repeated: true, type: Grpc.Testing.RequestResultCount -end - -defmodule Grpc.Testing.ClientType do - @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - field :SYNC_CLIENT, 0 - field :ASYNC_CLIENT, 1 - field :OTHER_CLIENT, 2 -end + field :client_success, 7, repeated: true, type: :bool, json_name: "clientSuccess" + field :server_success, 8, repeated: true, type: :bool, json_name: "serverSuccess" -defmodule Grpc.Testing.ServerType do - @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - field :SYNC_SERVER, 0 - field :ASYNC_SERVER, 1 - field :ASYNC_GENERIC_SERVER, 2 - field :OTHER_SERVER, 3 -end - -defmodule Grpc.Testing.RpcType do - @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - field :UNARY, 0 - field :STREAMING, 1 - field :STREAMING_FROM_CLIENT, 2 - field :STREAMING_FROM_SERVER, 3 - field :STREAMING_BOTH_WAYS, 4 + field :request_results, 9, + repeated: true, + type: Grpc.Testing.RequestResultCount, + json_name: "requestResults" end diff --git a/benchmark/lib/grpc/testing/messages.pb.ex b/benchmark/lib/grpc/testing/messages.pb.ex index 8c5021b3e..cb21c1aca 100644 --- a/benchmark/lib/grpc/testing/messages.pb.ex +++ b/benchmark/lib/grpc/testing/messages.pb.ex @@ -1,24 +1,23 @@ +defmodule Grpc.Testing.PayloadType do + @moduledoc false + + use Protobuf, enum: true, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :COMPRESSABLE, 0 +end + defmodule Grpc.Testing.BoolValue do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - value: boolean - } - defstruct [:value] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :value, 1, type: :bool end defmodule Grpc.Testing.Payload do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - type: atom | integer, - body: binary - } - defstruct [:type, :body] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :type, 1, type: Grpc.Testing.PayloadType, enum: true field :body, 2, type: :bytes @@ -26,13 +25,8 @@ end defmodule Grpc.Testing.EchoStatus do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - code: integer, - message: String.t() - } - defstruct [:code, :message] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :code, 1, type: :int32 field :message, 2, type: :string @@ -40,156 +34,93 @@ end defmodule Grpc.Testing.SimpleRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - response_type: atom | integer, - response_size: integer, - payload: Grpc.Testing.Payload.t() | nil, - fill_username: boolean, - fill_oauth_scope: boolean, - response_compressed: Grpc.Testing.BoolValue.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [ - :response_type, - :response_size, - :payload, - :fill_username, - :fill_oauth_scope, - :response_compressed, - :response_status, - :expect_compressed - ] - - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_size, 2, type: :int32 + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true + field :response_size, 2, type: :int32, json_name: "responseSize" field :payload, 3, type: Grpc.Testing.Payload - field :fill_username, 4, type: :bool - field :fill_oauth_scope, 5, type: :bool - field :response_compressed, 6, type: Grpc.Testing.BoolValue - field :response_status, 7, type: Grpc.Testing.EchoStatus - field :expect_compressed, 8, type: Grpc.Testing.BoolValue + field :fill_username, 4, type: :bool, json_name: "fillUsername" + field :fill_oauth_scope, 5, type: :bool, json_name: "fillOauthScope" + field :response_compressed, 6, type: Grpc.Testing.BoolValue, json_name: "responseCompressed" + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" + field :expect_compressed, 8, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" end defmodule Grpc.Testing.SimpleResponse do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - username: String.t(), - oauth_scope: String.t() - } - defstruct [:payload, :username, :oauth_scope] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload field :username, 2, type: :string - field :oauth_scope, 3, type: :string + field :oauth_scope, 3, type: :string, json_name: "oauthScope" end defmodule Grpc.Testing.StreamingInputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:payload, :expect_compressed] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload - field :expect_compressed, 2, type: Grpc.Testing.BoolValue + field :expect_compressed, 2, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" end defmodule Grpc.Testing.StreamingInputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - aggregated_payload_size: integer - } - defstruct [:aggregated_payload_size] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :aggregated_payload_size, 1, type: :int32 + field :aggregated_payload_size, 1, type: :int32, json_name: "aggregatedPayloadSize" end defmodule Grpc.Testing.ResponseParameters do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - size: integer, - interval_us: integer, - compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:size, :interval_us, :compressed] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :size, 1, type: :int32 - field :interval_us, 2, type: :int32 + field :interval_us, 2, type: :int32, json_name: "intervalUs" field :compressed, 3, type: Grpc.Testing.BoolValue end defmodule Grpc.Testing.StreamingOutputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - response_type: atom | integer, - response_parameters: [Grpc.Testing.ResponseParameters.t()], - payload: Grpc.Testing.Payload.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil - } - defstruct [:response_type, :response_parameters, :payload, :response_status] - - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_parameters, 2, repeated: true, type: Grpc.Testing.ResponseParameters + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true + + field :response_parameters, 2, + repeated: true, + type: Grpc.Testing.ResponseParameters, + json_name: "responseParameters" + field :payload, 3, type: Grpc.Testing.Payload - field :response_status, 7, type: Grpc.Testing.EchoStatus + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" end defmodule Grpc.Testing.StreamingOutputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil - } - defstruct [:payload] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload end defmodule Grpc.Testing.ReconnectParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - max_reconnect_backoff_ms: integer - } - defstruct [:max_reconnect_backoff_ms] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :max_reconnect_backoff_ms, 1, type: :int32 + field :max_reconnect_backoff_ms, 1, type: :int32, json_name: "maxReconnectBackoffMs" end defmodule Grpc.Testing.ReconnectInfo do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - passed: boolean, - backoff_ms: [integer] - } - defstruct [:passed, :backoff_ms] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :passed, 1, type: :bool - field :backoff_ms, 2, repeated: true, type: :int32 -end - -defmodule Grpc.Testing.PayloadType do - @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - field :COMPRESSABLE, 0 + field :backoff_ms, 2, repeated: true, type: :int32, json_name: "backoffMs" end diff --git a/benchmark/lib/grpc/testing/payloads.pb.ex b/benchmark/lib/grpc/testing/payloads.pb.ex index 18a1a63e1..65a0abbc5 100644 --- a/benchmark/lib/grpc/testing/payloads.pb.ex +++ b/benchmark/lib/grpc/testing/payloads.pb.ex @@ -1,50 +1,46 @@ defmodule Grpc.Testing.ByteBufferParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - req_size: integer, - resp_size: integer - } - defstruct [:req_size, :resp_size] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :req_size, 1, type: :int32 - field :resp_size, 2, type: :int32 + field :req_size, 1, type: :int32, json_name: "reqSize" + field :resp_size, 2, type: :int32, json_name: "respSize" end defmodule Grpc.Testing.SimpleProtoParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - req_size: integer, - resp_size: integer - } - defstruct [:req_size, :resp_size] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :req_size, 1, type: :int32 - field :resp_size, 2, type: :int32 + field :req_size, 1, type: :int32, json_name: "reqSize" + field :resp_size, 2, type: :int32, json_name: "respSize" end defmodule Grpc.Testing.ComplexProtoParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 end defmodule Grpc.Testing.PayloadConfig do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - payload: {atom, any} - } - defstruct [:payload] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 oneof :payload, 0 - field :bytebuf_params, 1, type: Grpc.Testing.ByteBufferParams, oneof: 0 - field :simple_params, 2, type: Grpc.Testing.SimpleProtoParams, oneof: 0 - field :complex_params, 3, type: Grpc.Testing.ComplexProtoParams, oneof: 0 + + field :bytebuf_params, 1, + type: Grpc.Testing.ByteBufferParams, + json_name: "bytebufParams", + oneof: 0 + + field :simple_params, 2, + type: Grpc.Testing.SimpleProtoParams, + json_name: "simpleParams", + oneof: 0 + + field :complex_params, 3, + type: Grpc.Testing.ComplexProtoParams, + json_name: "complexParams", + oneof: 0 end diff --git a/benchmark/lib/grpc/testing/stats.pb.ex b/benchmark/lib/grpc/testing/stats.pb.ex index 23418c6ee..9878eec0d 100644 --- a/benchmark/lib/grpc/testing/stats.pb.ex +++ b/benchmark/lib/grpc/testing/stats.pb.ex @@ -1,113 +1,63 @@ defmodule Grpc.Testing.ServerStats do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - time_elapsed: float, - time_user: float, - time_system: float, - total_cpu_time: non_neg_integer, - idle_cpu_time: non_neg_integer, - cq_poll_count: non_neg_integer, - core_stats: Grpc.Core.Stats.t() | nil - } - defstruct [ - :time_elapsed, - :time_user, - :time_system, - :total_cpu_time, - :idle_cpu_time, - :cq_poll_count, - :core_stats - ] - - field :time_elapsed, 1, type: :double - field :time_user, 2, type: :double - field :time_system, 3, type: :double - field :total_cpu_time, 4, type: :uint64 - field :idle_cpu_time, 5, type: :uint64 - field :cq_poll_count, 6, type: :uint64 - field :core_stats, 7, type: Grpc.Core.Stats + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :time_elapsed, 1, type: :double, json_name: "timeElapsed" + field :time_user, 2, type: :double, json_name: "timeUser" + field :time_system, 3, type: :double, json_name: "timeSystem" + field :total_cpu_time, 4, type: :uint64, json_name: "totalCpuTime" + field :idle_cpu_time, 5, type: :uint64, json_name: "idleCpuTime" + field :cq_poll_count, 6, type: :uint64, json_name: "cqPollCount" + field :core_stats, 7, type: Grpc.Core.Stats, json_name: "coreStats" end defmodule Grpc.Testing.HistogramParams do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - resolution: float, - max_possible: float - } - defstruct [:resolution, :max_possible] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :resolution, 1, type: :double - field :max_possible, 2, type: :double + field :max_possible, 2, type: :double, json_name: "maxPossible" end defmodule Grpc.Testing.HistogramData do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - bucket: [non_neg_integer], - min_seen: float, - max_seen: float, - sum: float, - sum_of_squares: float, - count: float - } - defstruct [:bucket, :min_seen, :max_seen, :sum, :sum_of_squares, :count] + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :bucket, 1, repeated: true, type: :uint32 - field :min_seen, 2, type: :double - field :max_seen, 3, type: :double + field :min_seen, 2, type: :double, json_name: "minSeen" + field :max_seen, 3, type: :double, json_name: "maxSeen" field :sum, 4, type: :double - field :sum_of_squares, 5, type: :double + field :sum_of_squares, 5, type: :double, json_name: "sumOfSquares" field :count, 6, type: :double end defmodule Grpc.Testing.RequestResultCount do @moduledoc false - use Protobuf, syntax: :proto3 - @type t :: %__MODULE__{ - status_code: integer, - count: integer - } - defstruct [:status_code, :count] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :status_code, 1, type: :int32 + field :status_code, 1, type: :int32, json_name: "statusCode" field :count, 2, type: :int64 end defmodule Grpc.Testing.ClientStats do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - latencies: Grpc.Testing.HistogramData.t() | nil, - time_elapsed: float, - time_user: float, - time_system: float, - request_results: [Grpc.Testing.RequestResultCount.t()], - cq_poll_count: non_neg_integer, - core_stats: Grpc.Core.Stats.t() | nil - } - defstruct [ - :latencies, - :time_elapsed, - :time_user, - :time_system, - :request_results, - :cq_poll_count, - :core_stats - ] + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 field :latencies, 1, type: Grpc.Testing.HistogramData - field :time_elapsed, 2, type: :double - field :time_user, 3, type: :double - field :time_system, 4, type: :double - field :request_results, 5, repeated: true, type: Grpc.Testing.RequestResultCount - field :cq_poll_count, 6, type: :uint64 - field :core_stats, 7, type: Grpc.Core.Stats + field :time_elapsed, 2, type: :double, json_name: "timeElapsed" + field :time_user, 3, type: :double, json_name: "timeUser" + field :time_system, 4, type: :double, json_name: "timeSystem" + + field :request_results, 5, + repeated: true, + type: Grpc.Testing.RequestResultCount, + json_name: "requestResults" + + field :cq_poll_count, 6, type: :uint64, json_name: "cqPollCount" + field :core_stats, 7, type: Grpc.Core.Stats, json_name: "coreStats" end diff --git a/benchmark/lib/grpc/testing/worker_service.pb.ex b/benchmark/lib/grpc/testing/worker_service.pb.ex index e6ebcf148..8b1378917 100644 --- a/benchmark/lib/grpc/testing/worker_service.pb.ex +++ b/benchmark/lib/grpc/testing/worker_service.pb.ex @@ -1,14 +1 @@ -defmodule Grpc.Testing.WorkerService.Service do - @moduledoc false - use GRPC.Service, name: "grpc.testing.WorkerService" - rpc :RunServer, stream(Grpc.Testing.ServerArgs), stream(Grpc.Testing.ServerStatus) - rpc :RunClient, stream(Grpc.Testing.ClientArgs), stream(Grpc.Testing.ClientStatus) - rpc :CoreCount, Grpc.Testing.CoreRequest, Grpc.Testing.CoreResponse - rpc :QuitWorker, Grpc.Testing.Void, Grpc.Testing.Void -end - -defmodule Grpc.Testing.WorkerService.Stub do - @moduledoc false - use GRPC.Stub, service: Grpc.Testing.WorkerService.Service -end diff --git a/benchmark/mix.exs b/benchmark/mix.exs index 9ed822e3f..316600450 100644 --- a/benchmark/mix.exs +++ b/benchmark/mix.exs @@ -22,7 +22,7 @@ defmodule Benchmark.MixProject do defp deps do [ {:grpc, path: ".."}, - {:protobuf, github: "tony612/protobuf-elixir", override: true} + {:protobuf, "~> 0.14"} ] end end diff --git a/benchmark/mix.lock b/benchmark/mix.lock index 32da1efe6..eeb146048 100644 --- a/benchmark/mix.lock +++ b/benchmark/mix.lock @@ -1,7 +1,10 @@ %{ - "cowboy": {:git, "https://github.com/elixir-grpc/cowboy.git", "db1b09fb06038415e5c643282554c0b9f8e6a976", [tag: "grpc-2.6.3"]}, - "cowlib": {:git, "https://github.com/elixir-grpc/cowlib.git", "1cc32e27d917bfe615da6957006fd9f8d6e604bd", [tag: "grpc-2.7.3"]}, - "gun": {:git, "https://github.com/elixir-grpc/gun.git", "975177bd5c179c800c8685cf64376689642ac972", [tag: "grpc-1.3.2"]}, - "protobuf": {:git, "https://github.com/tony612/protobuf-elixir.git", "384f97ca03aa4f874e527e9f28f5ebbae2f142f1", []}, - "ranch": {:git, "https://github.com/ninenines/ranch", "3190aef88aea04d6dce8545fe9b4574288903f44", [ref: "1.7.1"]}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/config/config.exs b/config/config.exs index ef59900fe..773875cb9 100644 --- a/config/config.exs +++ b/config/config.exs @@ -1,3 +1,7 @@ import Config -config :grpc, http2_client_adapter: GRPC.Adapter.Gun +config_file = Path.expand("#{config_env()}.exs", __DIR__) + +if File.exists?(config_file) do + import_config config_file +end diff --git a/config/test.exs b/config/test.exs new file mode 100644 index 000000000..477c90797 --- /dev/null +++ b/config/test.exs @@ -0,0 +1,3 @@ +import Config + +config :logger, level: :info diff --git a/examples/helloworld/.gitignore b/examples/helloworld/.gitignore index 62deea564..06dbcb6f6 100644 --- a/examples/helloworld/.gitignore +++ b/examples/helloworld/.gitignore @@ -20,4 +20,4 @@ erl_crash.dump /src/grpc_c /tmp -/log +/log \ No newline at end of file diff --git a/examples/helloworld/README.md b/examples/helloworld/README.md index 8eaa2c265..e3c2b3454 100644 --- a/examples/helloworld/README.md +++ b/examples/helloworld/README.md @@ -9,7 +9,7 @@ $ mix do deps.get, compile 2. Run the server ```shell -$ mix grpc.server +$ mix run --no-halt ``` 3. Run the client script @@ -17,6 +17,16 @@ $ mix grpc.server $ mix run priv/client.exs ``` +## HTTP Transcoding + +``` shell +# Say hello +curl -H 'Content-type: application/json' http://localhost:50051/v1/greeter/test + +# Say hello from +curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50051/v1/greeter +``` + ## Regenerate Elixir code from proto 1. Modify the proto `priv/protos/helloworld.proto` @@ -26,19 +36,17 @@ $ mix run priv/client.exs mix escript.install hex protobuf ``` 4. Generate the code: + ```shell -$ protoc -I priv/protos --elixir_out=plugins=grpc:./lib/ priv/protos/helloworld.proto +$ (cd ../../; mix build_protobuf_escript && mix escript.build) +$ protoc -I priv/protos --elixir_out=:./lib/ --grpc_elixir_out=./lib --plugin="../../deps/protobuf/protoc-gen-elixir" --plugin="../../protoc-gen-grpc_elixir" priv/protos/helloworld.proto ``` Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for more information. ## How to start server when starting your application? -Change the config to: - -```elixir -config :grpc, start_server: true -``` +Pass `start_server: true` as an option for the `GRPC.Server.Supervisor` in your supervision tree. ## Benchmark diff --git a/examples/helloworld/config/config.exs b/examples/helloworld/config/config.exs index c2f683e97..9def7c2c5 100644 --- a/examples/helloworld/config/config.exs +++ b/examples/helloworld/config/config.exs @@ -1,6 +1,3 @@ -use Mix.Config - -# Start server in OTP -# config :grpc, start_server: true +import Config import_config "#{Mix.env}.exs" diff --git a/examples/helloworld/config/dev.exs b/examples/helloworld/config/dev.exs index d2d855e6d..becde7693 100644 --- a/examples/helloworld/config/dev.exs +++ b/examples/helloworld/config/dev.exs @@ -1 +1 @@ -use Mix.Config +import Config diff --git a/examples/helloworld/config/prod.exs b/examples/helloworld/config/prod.exs index 9d6d504af..2c946e805 100644 --- a/examples/helloworld/config/prod.exs +++ b/examples/helloworld/config/prod.exs @@ -1,6 +1,4 @@ -use Mix.Config - -config :grpc, start_server: true +import Config config :logger, - level: :warn + level: :warning diff --git a/examples/helloworld/config/test.exs b/examples/helloworld/config/test.exs new file mode 100644 index 000000000..becde7693 --- /dev/null +++ b/examples/helloworld/config/test.exs @@ -0,0 +1 @@ +import Config diff --git a/examples/helloworld/lib/endpoint.ex b/examples/helloworld/lib/endpoint.ex index 70533a487..c8bc64f69 100644 --- a/examples/helloworld/lib/endpoint.ex +++ b/examples/helloworld/lib/endpoint.ex @@ -1,6 +1,6 @@ defmodule Helloworld.Endpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Server.Interceptors.Logger run Helloworld.Greeter.Server end diff --git a/examples/helloworld/lib/helloworld.pb.ex b/examples/helloworld/lib/helloworld.pb.ex index 7c65811d0..9a10f57ba 100644 --- a/examples/helloworld/lib/helloworld.pb.ex +++ b/examples/helloworld/lib/helloworld.pb.ex @@ -1,31 +1,41 @@ defmodule Helloworld.HelloRequest do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - name: String.t() - } - defstruct [:name] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :name, 1, optional: true, type: :string + field :name, 1, type: :string +end + +defmodule Helloworld.HelloRequestFrom do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string + field :from, 2, type: :string end defmodule Helloworld.HelloReply do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - message: String.t() - } - defstruct [:message] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :message, 1, optional: true, type: :string + field :message, 1, type: :string + field :today, 2, type: Google.Protobuf.Timestamp end -defmodule Helloworld.Greeter.Service do - use GRPC.Service, name: "helloworld.Greeter" +defmodule Helloworld.GetMessageRequest do + @moduledoc false - rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string end -defmodule Helloworld.Greeter.Stub do - use GRPC.Stub, service: Helloworld.Greeter.Service +defmodule Helloworld.Message do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :text, 1, type: :string end diff --git a/examples/helloworld/lib/helloworld.svc.ex b/examples/helloworld/lib/helloworld.svc.ex new file mode 100644 index 000000000..dcf882ab1 --- /dev/null +++ b/examples/helloworld/lib/helloworld.svc.ex @@ -0,0 +1,65 @@ +defmodule Helloworld.Greeter.Service do + @moduledoc false + + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.11.0" + + rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + __unknown_fields__: [], + additional_bindings: [], + body: "", + pattern: {:get, "/v1/greeter/{name}"}, + response_body: "", + selector: "" + } + } + }) + + rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + __unknown_fields__: [], + additional_bindings: [], + body: "*", + pattern: {:post, "/v1/greeter"}, + response_body: "", + selector: "" + } + } + }) +end + +defmodule Helloworld.Greeter.Stub do + @moduledoc false + + use GRPC.Stub, service: Helloworld.Greeter.Service +end + +defmodule Helloworld.Messaging.Service do + @moduledoc false + + use GRPC.Service, name: "helloworld.Messaging", protoc_gen_elixir_version: "0.11.0" + + rpc(:GetMessage, Helloworld.GetMessageRequest, Helloworld.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + __unknown_fields__: [], + additional_bindings: [], + body: "", + pattern: {:get, "/v1/{name=messages/*}"}, + response_body: "", + selector: "" + } + } + }) +end + +defmodule Helloworld.Messaging.Stub do + @moduledoc false + + use GRPC.Stub, service: Helloworld.Messaging.Service +end diff --git a/examples/helloworld/lib/helloworld_app.ex b/examples/helloworld/lib/helloworld_app.ex index 93500609c..d84d62a58 100644 --- a/examples/helloworld/lib/helloworld_app.ex +++ b/examples/helloworld/lib/helloworld_app.ex @@ -2,10 +2,8 @@ defmodule HelloworldApp do use Application def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(GRPC.Server.Supervisor, [{Helloworld.Endpoint, 50051}]) + {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true} ] opts = [strategy: :one_for_one, name: HelloworldApp] diff --git a/examples/helloworld/lib/server.ex b/examples/helloworld/lib/server.ex index 7b081083c..d7b828907 100644 --- a/examples/helloworld/lib/server.ex +++ b/examples/helloworld/lib/server.ex @@ -1,5 +1,7 @@ defmodule Helloworld.Greeter.Server do - use GRPC.Server, service: Helloworld.Greeter.Service + use GRPC.Server, + service: Helloworld.Greeter.Service, + http_transcode: true @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) :: Helloworld.HelloReply.t() @@ -13,6 +15,26 @@ defmodule Helloworld.Greeter.Server do end def say_hello(request, _stream) do - Helloworld.HelloReply.new(message: "Hello #{request.name}") + %Helloworld.HelloReply{ + message: "Hello #{request.name}", + today: today() + } + end + + @spec say_hello_from(Helloworld.HelloFromRequest.t(), GRPC.Server.Stream.t()) :: + Helloworld.HelloReply.t() + def say_hello_from(request, _stream) do + %Helloworld.HelloReply{ + message: "Hello #{request.name}. From #{request.from}", + today: today() + } + end + + defp today do + nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond) + seconds = div(nanos_epoch, 1_000_000_000) + nanos = nanos_epoch - seconds * 1_000_000_000 + + %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos} end end diff --git a/examples/helloworld/mix.exs b/examples/helloworld/mix.exs index df5080df5..78e2443c3 100644 --- a/examples/helloworld/mix.exs +++ b/examples/helloworld/mix.exs @@ -2,26 +2,26 @@ defmodule Helloworld.Mixfile do use Mix.Project def project do - [app: :helloworld, - version: "0.1.0", - elixir: "~> 1.4", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps()] + [ + app: :helloworld, + version: "0.1.0", + elixir: "~> 1.4", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] end def application do - [mod: {HelloworldApp, []}, - applications: [:logger, :grpc]] + [mod: {HelloworldApp, []}, applications: [:logger, :grpc]] end defp deps do [ {:grpc, path: "../../"}, - {:protobuf, github: "tony612/protobuf-elixir", override: true}, - {:google_protos, "~> 0.1.0"}, - {:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:jason, "~> 1.3.0"}, + {:protobuf, "~> 0.14"}, + {:google_protos, "~> 0.3.0"}, ] end end diff --git a/examples/helloworld/mix.lock b/examples/helloworld/mix.lock index 33b83fade..dbf35d41b 100644 --- a/examples/helloworld/mix.lock +++ b/examples/helloworld/mix.lock @@ -1,9 +1,12 @@ %{ - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, - "cowlib": {:hex, :grpc_cowlib, "2.8.1", "ddaf77f3b89bd8e6c76df67b28a4b069688eef91c0c497a246cf9bfcdf87f7d3", [:rebar3], [], "hexpm", "0366f82719d24af4ce45a6591f52a7fc859785823fde4cd84e0dc45119b5ed89"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm", "6c32a70ed5d452c6650916555b1f96c79af5fc4bf286997f8b15f213de786f73"}, - "google_protos": {:hex, :google_protos, "0.1.0", "c6b9e12092d17571b093d4156d004494ca143b65dbbcbfc3ffff463ea03467c0", [:mix], [{:protobuf, "~> 0.5", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "ff5564525f89d2638a4cfa9fb4d31e9ee9d9d7cb937b3e8a95f558440c039e1b"}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "03dbbca1a9c604a0267a40ea1d69986225091acb822de0b2dbea21d5815e410b"}, - "protobuf": {:git, "https://github.com/tony612/protobuf-elixir.git", "384f97ca03aa4f874e527e9f28f5ebbae2f142f1", []}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, + "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/examples/helloworld/priv/client.exs b/examples/helloworld/priv/client.exs index cfb8560e0..0209556fc 100644 --- a/examples/helloworld/priv/client.exs +++ b/examples/helloworld/priv/client.exs @@ -1,6 +1,9 @@ -{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) +{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger]) {:ok, reply} = - channel |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir")) + channel + |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir")) + +# pass tuple `timeout: :infinity` as a second arg to stay in IEx debugging IO.inspect(reply) diff --git a/examples/helloworld/priv/protos/google/api/annotations.proto b/examples/helloworld/priv/protos/google/api/annotations.proto new file mode 100644 index 000000000..efdab3db6 --- /dev/null +++ b/examples/helloworld/priv/protos/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/examples/helloworld/priv/protos/google/api/http.proto b/examples/helloworld/priv/protos/google/api/http.proto new file mode 100644 index 000000000..113fa936a --- /dev/null +++ b/examples/helloworld/priv/protos/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/examples/helloworld/priv/protos/helloworld.proto b/examples/helloworld/priv/protos/helloworld.proto index 688974b28..632519a06 100644 --- a/examples/helloworld/priv/protos/helloworld.proto +++ b/examples/helloworld/priv/protos/helloworld.proto @@ -5,12 +5,26 @@ option java_package = "io.grpc.examples.helloworld"; option java_outer_classname = "HelloWorldProto"; option objc_class_prefix = "HLW"; +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + package helloworld; // The greeting service definition. service Greeter { // Sends a greeting - rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter/{name}" + }; + } + + rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/greeter" + body: "*" + }; + } } // The request message containing the user's name. @@ -18,7 +32,31 @@ message HelloRequest { string name = 1; } +// HelloRequestFrom! +message HelloRequestFrom { + // Name! + string name = 1; + // From! + string from = 2; +} + // The response message containing the greetings message HelloReply { string message = 1; + google.protobuf.Timestamp today = 2; +} + +service Messaging { + rpc GetMessage(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/{name=messages/*}" + }; + } +} + +message GetMessageRequest { + string name = 1; // Mapped to URL path. +} +message Message { + string text = 1; // The resource content. } diff --git a/examples/helloworld/test/hello_world_test.exs b/examples/helloworld/test/hello_world_test.exs new file mode 100644 index 000000000..55c51ed54 --- /dev/null +++ b/examples/helloworld/test/hello_world_test.exs @@ -0,0 +1,16 @@ +defmodule HelloworldTest do + @moduledoc false + + use ExUnit.Case + + setup_all do + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger]) + [channel: channel] + end + + test "helloworld should be successful", %{channel: channel} do + req = Helloworld.HelloRequest.new(name: "grpc-elixir") + assert {:ok, %{message: msg, today: _}} = Helloworld.Greeter.Stub.say_hello(channel, req) + assert msg == "Hello grpc-elixir" + end +end diff --git a/examples/helloworld/test/test_helper.exs b/examples/helloworld/test/test_helper.exs new file mode 100644 index 000000000..869559e70 --- /dev/null +++ b/examples/helloworld/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/examples/helloworld_streams/.formatter.exs b/examples/helloworld_streams/.formatter.exs new file mode 100644 index 000000000..d2cda26ed --- /dev/null +++ b/examples/helloworld_streams/.formatter.exs @@ -0,0 +1,4 @@ +# Used by "mix format" +[ + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] +] diff --git a/examples/helloworld_streams/.gitignore b/examples/helloworld_streams/.gitignore new file mode 100644 index 000000000..965abf9ca --- /dev/null +++ b/examples/helloworld_streams/.gitignore @@ -0,0 +1,26 @@ +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where third-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Ignore package tarball (built via "mix hex.build"). +helloworld_streams-*.tar + +# Temporary files, for example, from tests. +/tmp/ diff --git a/examples/helloworld_streams/README.md b/examples/helloworld_streams/README.md new file mode 100644 index 000000000..79b5c6342 --- /dev/null +++ b/examples/helloworld_streams/README.md @@ -0,0 +1,21 @@ +# HelloworldStreams + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed +by adding `helloworld_streams` to your list of dependencies in `mix.exs`: + +```elixir +def deps do + [ + {:helloworld_streams, "~> 0.1.0"} + ] +end +``` + +Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) +and published on [HexDocs](https://hexdocs.pm). Once published, the docs can +be found at . + diff --git a/examples/helloworld_streams/lib/helloworld_streams.ex b/examples/helloworld_streams/lib/helloworld_streams.ex new file mode 100644 index 000000000..8c1285a1c --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams.ex @@ -0,0 +1,5 @@ +defmodule HelloworldStreams do + @moduledoc """ + Documentation for `HelloworldStreams`. + """ +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/application.ex b/examples/helloworld_streams/lib/helloworld_streams/application.ex new file mode 100644 index 000000000..44cfaa165 --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/application.ex @@ -0,0 +1,19 @@ +defmodule HelloworldStreams.Application do + @moduledoc false + use Application + + @impl true + def start(_type, _args) do + children = [ + HelloworldStreams.Utils.Transformer, + GrpcReflection, + { + GRPC.Server.Supervisor, + endpoint: HelloworldStreams.Endpoint, port: 50053, start_server: true + } + ] + + opts = [strategy: :one_for_one, name: HelloworldStreams.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex b/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex new file mode 100644 index 000000000..c02a0c9bc --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/endpoint.ex @@ -0,0 +1,8 @@ +defmodule HelloworldStreams.Endpoint do + @moduledoc false + use GRPC.Endpoint + + intercept(GRPC.Server.Interceptors.Logger) + run(HelloworldStreams.Utils.Reflection) + run(HelloworldStreams.Server) +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/server.ex b/examples/helloworld_streams/lib/helloworld_streams/server.ex new file mode 100644 index 000000000..76771939f --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/server.ex @@ -0,0 +1,61 @@ +defmodule HelloworldStreams.Server do + @moduledoc """ + gRPC service for streaming data. + """ + use GRPC.Server, service: Stream.EchoServer.Service + + alias HelloworldStreams.Utils.Transformer + alias GRPC.Stream, as: GRPCStream + + alias Stream.HelloRequest + alias Stream.HelloReply + + @spec say_unary_hello(HelloRequest.t(), GRPC.Server.Stream.t()) :: any() + def say_unary_hello(request, _materializer) do + GRPCStream.unary(request) + |> GRPCStream.ask(Transformer) + |> GRPCStream.map(fn %HelloReply{} = reply -> + %HelloReply{message: "[Reply] #{reply.message}"} + end) + |> GRPCStream.run() + end + + @spec say_server_hello(HelloRequest.t(), GRPC.Server.Stream.t()) :: any() + def say_server_hello(request, materializer) do + create_output_stream(request) + |> GRPCStream.from() + |> GRPCStream.run_with(materializer) + end + + defp create_output_stream(msg) do + Stream.repeatedly(fn -> + index = :rand.uniform(10) + %HelloReply{message: "[#{index}] I'm the Server for #{msg.name}"} + end) + |> Stream.take(10) + |> Enum.to_list() + end + + @spec say_bid_stream_hello(Enumerable.t(), GRPC.Server.Stream.t()) :: any() + def say_bid_stream_hello(request, materializer) do + # simulate a infinite stream of data + # this is a simple example, in a real world application + # you would probably use a GenStage or similar + # to handle the stream of data + output_stream = + Stream.repeatedly(fn -> + index = :rand.uniform(10) + %HelloReply{message: "[#{index}] I'm the Server ;)"} + end) + + GRPCStream.from(request, join_with: output_stream) + |> GRPCStream.map(fn + %HelloRequest{} = hello -> + %HelloReply{message: "Welcome #{hello.name}"} + + output_item -> + output_item + end) + |> GRPCStream.run_with(materializer) + end +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex b/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex new file mode 100644 index 000000000..b060b543f --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/stream.pb.ex @@ -0,0 +1,156 @@ +defmodule Stream.HelloRequest do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3 + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.DescriptorProto{ + name: "HelloRequest", + field: [ + %Google.Protobuf.FieldDescriptorProto{ + name: "name", + extendee: nil, + number: 1, + label: :LABEL_OPTIONAL, + type: :TYPE_STRING, + type_name: nil, + default_value: nil, + options: nil, + oneof_index: nil, + json_name: "name", + proto3_optional: nil, + __unknown_fields__: [] + } + ], + nested_type: [], + enum_type: [], + extension_range: [], + extension: [], + options: nil, + oneof_decl: [], + reserved_range: [], + reserved_name: [], + __unknown_fields__: [] + } + end + + field(:name, 1, type: :string) +end + +defmodule Stream.HelloReply do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.14.0", syntax: :proto3 + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.DescriptorProto{ + name: "HelloReply", + field: [ + %Google.Protobuf.FieldDescriptorProto{ + name: "message", + extendee: nil, + number: 1, + label: :LABEL_OPTIONAL, + type: :TYPE_STRING, + type_name: nil, + default_value: nil, + options: nil, + oneof_index: nil, + json_name: "message", + proto3_optional: nil, + __unknown_fields__: [] + } + ], + nested_type: [], + enum_type: [], + extension_range: [], + extension: [], + options: nil, + oneof_decl: [], + reserved_range: [], + reserved_name: [], + __unknown_fields__: [] + } + end + + field(:message, 1, type: :string) +end + +defmodule Stream.EchoServer.Service do + @moduledoc false + + use GRPC.Service, name: "stream.EchoServer", protoc_gen_elixir_version: "0.14.0" + + def descriptor do + # credo:disable-for-next-line + %Google.Protobuf.ServiceDescriptorProto{ + name: "EchoServer", + method: [ + %Google.Protobuf.MethodDescriptorProto{ + name: "SayUnaryHello", + input_type: ".stream.HelloRequest", + output_type: ".stream.HelloReply", + options: %Google.Protobuf.MethodOptions{ + deprecated: false, + idempotency_level: :IDEMPOTENCY_UNKNOWN, + features: nil, + uninterpreted_option: [], + __pb_extensions__: %{}, + __unknown_fields__: [] + }, + client_streaming: false, + server_streaming: false, + __unknown_fields__: [] + }, + %Google.Protobuf.MethodDescriptorProto{ + name: "SayServerHello", + input_type: ".stream.HelloRequest", + output_type: ".stream.HelloReply", + options: %Google.Protobuf.MethodOptions{ + deprecated: false, + idempotency_level: :IDEMPOTENCY_UNKNOWN, + features: nil, + uninterpreted_option: [], + __pb_extensions__: %{}, + __unknown_fields__: [] + }, + client_streaming: false, + server_streaming: true, + __unknown_fields__: [] + }, + %Google.Protobuf.MethodDescriptorProto{ + name: "SayBidStreamHello", + input_type: ".stream.HelloRequest", + output_type: ".stream.HelloReply", + options: %Google.Protobuf.MethodOptions{ + deprecated: false, + idempotency_level: :IDEMPOTENCY_UNKNOWN, + features: nil, + uninterpreted_option: [], + __pb_extensions__: %{}, + __unknown_fields__: [] + }, + client_streaming: true, + server_streaming: true, + __unknown_fields__: [] + } + ], + options: nil, + __unknown_fields__: [] + } + end + + rpc(:SayUnaryHello, Stream.HelloRequest, Stream.HelloReply) + + rpc(:SayServerHello, Stream.HelloRequest, stream(Stream.HelloReply)) + + rpc(:SayBidStreamHello, stream(Stream.HelloRequest), stream(Stream.HelloReply)) +end + +defmodule Stream.EchoServer.Stub do + @moduledoc false + + use GRPC.Stub, service: Stream.EchoServer.Service +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex b/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex new file mode 100644 index 000000000..a5e25e598 --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/utils/reflection.ex @@ -0,0 +1,8 @@ +defmodule HelloworldStreams.Utils.Reflection do + @moduledoc """ + gRPC reflection server. + """ + use GrpcReflection.Server, + version: :v1, + services: [Stream.EchoServer.Service] +end diff --git a/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex b/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex new file mode 100644 index 000000000..ed9de0524 --- /dev/null +++ b/examples/helloworld_streams/lib/helloworld_streams/utils/transformer.ex @@ -0,0 +1,20 @@ +defmodule HelloworldStreams.Utils.Transformer do + @moduledoc """ + `Transformer` GenServer for example purposes. + """ + use GenServer + + alias Stream.HelloRequest + alias Stream.HelloReply + + def start_link(_) do + GenServer.start_link(__MODULE__, nil, name: __MODULE__) + end + + def init(_), do: {:ok, %{}} + + def handle_info({:request, %HelloRequest{} = value, from}, state) do + Process.send(from, {:response, %HelloReply{message: "Hello #{value.name}"}}, []) + {:noreply, state} + end +end diff --git a/examples/helloworld_streams/mix.exs b/examples/helloworld_streams/mix.exs new file mode 100644 index 000000000..8107d0bbd --- /dev/null +++ b/examples/helloworld_streams/mix.exs @@ -0,0 +1,28 @@ +defmodule HelloworldStreams.MixProject do + use Mix.Project + + def project do + [ + app: :helloworld_streams, + version: "0.1.0", + elixir: "~> 1.15", + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [ + extra_applications: [:logger], + mod: {HelloworldStreams.Application, []} + ] + end + + defp deps do + [ + {:grpc, path: "../../", override: true}, + {:protobuf, "~> 0.14"}, + {:grpc_reflection, "~> 0.1"} + ] + end +end diff --git a/examples/helloworld_streams/mix.lock b/examples/helloworld_streams/mix.lock new file mode 100644 index 000000000..ee24db4b2 --- /dev/null +++ b/examples/helloworld_streams/mix.lock @@ -0,0 +1,14 @@ +%{ + "cowboy": {:hex, :cowboy, "2.13.0", "09d770dd5f6a22cc60c071f432cd7cb87776164527f205c5a6b0f24ff6b38990", [:make, :rebar3], [{:cowlib, ">= 2.14.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e724d3a70995025d654c1992c7b11dbfea95205c047d86ff9bf1cda92ddc5614"}, + "cowlib": {:hex, :cowlib, "2.15.0", "3c97a318a933962d1c12b96ab7c1d728267d2c523c25a5b57b0f93392b6e9e25", [:make, :rebar3], [], "hexpm", "4f00c879a64b4fe7c8fcb42a4281925e9ffdb928820b03c3ad325a617e857532"}, + "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, + "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, + "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, + "grpc_reflection": {:hex, :grpc_reflection, "0.1.5", "d00cdf8ef2638edb9578248eedc742e1b34eda9100e61be764c552c10f4b46cb", [:mix], [{:grpc, "~> 0.9", [hex: :grpc, repo: "hexpm", optional: false]}, {:protobuf, "~> 0.14", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "848334d16029aee33728603be6171fc8bfcdfa3508cd6885ec1729e2e6ac60a5"}, + "gun": {:hex, :gun, "2.2.0", "b8f6b7d417e277d4c2b0dc3c07dfdf892447b087f1cc1caff9c0f556b884e33d", [:make, :rebar3], [{:cowlib, ">= 2.15.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "76022700c64287feb4df93a1795cff6741b83fb37415c40c34c38d2a4645261a"}, + "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, +} diff --git a/examples/helloworld_streams/priv/protos/stream.proto b/examples/helloworld_streams/priv/protos/stream.proto new file mode 100644 index 000000000..e0ad2ac0a --- /dev/null +++ b/examples/helloworld_streams/priv/protos/stream.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package stream; + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} + +service EchoServer { + rpc SayUnaryHello (HelloRequest) returns (HelloReply) {} + rpc SayServerHello (HelloRequest) returns (stream HelloReply) {} + rpc SayBidStreamHello (stream HelloRequest) returns (stream HelloReply) {} +} diff --git a/examples/helloworld_streams/test/helloworld_streams_test.exs b/examples/helloworld_streams/test/helloworld_streams_test.exs new file mode 100644 index 000000000..838751bbe --- /dev/null +++ b/examples/helloworld_streams/test/helloworld_streams_test.exs @@ -0,0 +1,4 @@ +defmodule HelloworldStreamsTest do + use ExUnit.Case + doctest HelloworldStreams +end diff --git a/examples/helloworld_streams/test/test_helper.exs b/examples/helloworld_streams/test/test_helper.exs new file mode 100644 index 000000000..869559e70 --- /dev/null +++ b/examples/helloworld_streams/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/examples/helloworld_transcoding/.gitignore b/examples/helloworld_transcoding/.gitignore new file mode 100644 index 000000000..06dbcb6f6 --- /dev/null +++ b/examples/helloworld_transcoding/.gitignore @@ -0,0 +1,23 @@ +# The directory Mix will write compiled artifacts to. +/_build + +# If you run "mix test --cover", coverage assets end up here. +/cover + +# The directory Mix downloads your dependencies sources to. +/deps + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +/priv/grpc_c.so* +/src/grpc_c +/tmp + +/log \ No newline at end of file diff --git a/examples/helloworld_transcoding/README.md b/examples/helloworld_transcoding/README.md new file mode 100644 index 000000000..66624ce0c --- /dev/null +++ b/examples/helloworld_transcoding/README.md @@ -0,0 +1,83 @@ +# Helloworld with HTTP/json transcoding in grpc-elixir + +## Usage + +1. Install deps and compile +```shell +$ mix do deps.get, compile +``` + +2. Run the server +```shell +$ mix run --no-halt +``` + +3. Run the client script +```shell +$ mix run priv/client.exs +``` + +## HTTP Transcoding + +``` shell +# Say hello +$ curl -H 'accept: application/json' http://localhost:50051/v1/greeter/test + +# Say hello from +$ curl -XPOST -H 'Content-type: application/json' -d '{"name": "test", "from": "anon"}' http://localhost:50051/v1/greeter +``` + +## Regenerate Elixir code from proto + +1. Modify the proto `priv/protos/helloworld.proto` + +2. Install `protoc` [here](https://developers.google.com/protocol-buffers/docs/downloads) + +``` +mix deps.get +``` + +4. Generate `google.api.http` extensions: + +``` shell +$ mix protobuf.generate --include-path=priv/protos --output-path=./lib priv/protos/google/api/annotations.proto priv/protos/google/api/http.proto +``` + +4. Generate the code: + +```shell +$ mix protobuf.generate --include-path=priv/protos --plugins=ProtobufGenerate.Plugins.GRPCWithOptions --output-path=./lib priv/protos/helloworld.proto +``` + +Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for more information. + +## How to start server when starting your application? + +Pass `start_server: true` as an option for the `GRPC.Server.Supervisor` in your supervision tree. + +## Benchmark + +Using [ghz](https://ghz.sh/) + +``` +$ MIX_ENV=prod iex -S mix +# Now cowboy doesn't work well with concurrency in a connection, like --concurrency 6 --connections 1 +$ ghz --insecure --proto priv/protos/helloworld.proto --call helloworld.Greeter.SayHello -d '{"name":"Joe"}' -z 10s --concurrency 6 --connections 6 127.0.0.1:50051 +# The result is for branch improve-perf +Summary: + Count: 124239 + Total: 10.00 s + Slowest: 18.85 ms + Fastest: 0.18 ms + Average: 0.44 ms + Requests/sec: 12423.71 + +# Go +Summary: + Count: 258727 + Total: 10.00 s + Slowest: 5.39 ms + Fastest: 0.09 ms + Average: 0.19 ms + Requests/sec: 25861.68 +``` diff --git a/examples/helloworld_transcoding/config/config.exs b/examples/helloworld_transcoding/config/config.exs new file mode 100644 index 000000000..9def7c2c5 --- /dev/null +++ b/examples/helloworld_transcoding/config/config.exs @@ -0,0 +1,3 @@ +import Config + +import_config "#{Mix.env}.exs" diff --git a/examples/helloworld_transcoding/config/dev.exs b/examples/helloworld_transcoding/config/dev.exs new file mode 100644 index 000000000..becde7693 --- /dev/null +++ b/examples/helloworld_transcoding/config/dev.exs @@ -0,0 +1 @@ +import Config diff --git a/examples/helloworld_transcoding/config/prod.exs b/examples/helloworld_transcoding/config/prod.exs new file mode 100644 index 000000000..2dd33c31d --- /dev/null +++ b/examples/helloworld_transcoding/config/prod.exs @@ -0,0 +1,4 @@ +import Config + +config :logger, + level: :warn diff --git a/examples/helloworld_transcoding/config/test.exs b/examples/helloworld_transcoding/config/test.exs new file mode 100644 index 000000000..becde7693 --- /dev/null +++ b/examples/helloworld_transcoding/config/test.exs @@ -0,0 +1 @@ +import Config diff --git a/examples/helloworld_transcoding/lib/endpoint.ex b/examples/helloworld_transcoding/lib/endpoint.ex new file mode 100644 index 000000000..c8bc64f69 --- /dev/null +++ b/examples/helloworld_transcoding/lib/endpoint.ex @@ -0,0 +1,6 @@ +defmodule Helloworld.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Server.Interceptors.Logger + run Helloworld.Greeter.Server +end diff --git a/examples/helloworld_transcoding/lib/helloworld.pb.ex b/examples/helloworld_transcoding/lib/helloworld.pb.ex new file mode 100644 index 000000000..ef45cc5ca --- /dev/null +++ b/examples/helloworld_transcoding/lib/helloworld.pb.ex @@ -0,0 +1,55 @@ +defmodule Helloworld.HelloRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string +end + +defmodule Helloworld.HelloRequestFrom do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string + field :from, 2, type: :string +end + +defmodule Helloworld.HelloReply do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :message, 1, type: :string + field :today, 2, type: Google.Protobuf.Timestamp +end + +defmodule Helloworld.Greeter.Service do + @moduledoc false + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.14.1" + + rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/greeter/{name}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:SayHelloFrom, Helloworld.HelloRequestFrom, Helloworld.HelloReply, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "*", + additional_bindings: [], + response_body: "", + pattern: {:post, "/v1/greeter"}, + __unknown_fields__: [] + } + } + }) +end diff --git a/examples/helloworld_transcoding/lib/helloworld_app.ex b/examples/helloworld_transcoding/lib/helloworld_app.ex new file mode 100644 index 000000000..d84d62a58 --- /dev/null +++ b/examples/helloworld_transcoding/lib/helloworld_app.ex @@ -0,0 +1,12 @@ +defmodule HelloworldApp do + use Application + + def start(_type, _args) do + children = [ + {GRPC.Server.Supervisor, endpoint: Helloworld.Endpoint, port: 50051, start_server: true} + ] + + opts = [strategy: :one_for_one, name: HelloworldApp] + Supervisor.start_link(children, opts) + end +end diff --git a/examples/helloworld_transcoding/lib/server.ex b/examples/helloworld_transcoding/lib/server.ex new file mode 100644 index 000000000..8786949fe --- /dev/null +++ b/examples/helloworld_transcoding/lib/server.ex @@ -0,0 +1,31 @@ +defmodule Helloworld.Greeter.Server do + use GRPC.Server, + service: Helloworld.Greeter.Service, + http_transcode: true + + @spec say_hello(Helloworld.HelloRequest.t(), GRPC.Server.Stream.t()) :: + Helloworld.HelloReply.t() + def say_hello(request, _stream) do + %Helloworld.HelloReply{ + message: "Hello #{request.name}", + today: today() + } + end + + @spec say_hello_from(Helloworld.HelloFromRequest.t(), GRPC.Server.Stream.t()) :: + Helloworld.HelloReply.t() + def say_hello_from(request, _stream) do + %Helloworld.HelloReply{ + message: "Hello #{request.name}. From #{request.from}", + today: today() + } + end + + defp today do + nanos_epoch = System.system_time() |> System.convert_time_unit(:native, :nanosecond) + seconds = div(nanos_epoch, 1_000_000_000) + nanos = nanos_epoch - seconds * 1_000_000_000 + + %Google.Protobuf.Timestamp{seconds: seconds, nanos: nanos} + end +end diff --git a/examples/helloworld_transcoding/mix.exs b/examples/helloworld_transcoding/mix.exs new file mode 100644 index 000000000..c67c06c53 --- /dev/null +++ b/examples/helloworld_transcoding/mix.exs @@ -0,0 +1,28 @@ +defmodule Helloworld.Mixfile do + use Mix.Project + + def project do + [ + app: :helloworld, + version: "0.1.0", + elixir: "~> 1.4", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] + end + + def application do + [mod: {HelloworldApp, []}, applications: [:logger, :grpc]] + end + + defp deps do + [ + {:grpc, path: "../../"}, + {:protobuf, "~> 0.14"}, + {:jason, "~> 1.3.0"}, + {:google_protos, "~> 0.3.0"}, + {:protobuf_generate, "~> 0.1", only: [:dev, :test]} + ] + end +end diff --git a/examples/helloworld_transcoding/mix.lock b/examples/helloworld_transcoding/mix.lock new file mode 100644 index 000000000..439d97100 --- /dev/null +++ b/examples/helloworld_transcoding/mix.lock @@ -0,0 +1,13 @@ +%{ + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "google_protos": {:hex, :google_protos, "0.3.0", "15faf44dce678ac028c289668ff56548806e313e4959a3aaf4f6e1ebe8db83f4", [:mix], [{:protobuf, "~> 0.10", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "1f6b7fb20371f72f418b98e5e48dae3e022a9a6de1858d4b254ac5a5d0b4035f"}, + "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "protobuf_generate": {:hex, :protobuf_generate, "0.1.1", "f6098b85161dcfd48a4f6f1abee4ee5e057981dfc50aafb1aa4bd5b0529aa89b", [:mix], [{:protobuf, "~> 0.11", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "93a38c8e2aba2a17e293e9ef1359122741f717103984aa6d1ebdca0efb17ab9d"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, +} diff --git a/examples/helloworld_transcoding/priv/client.exs b/examples/helloworld_transcoding/priv/client.exs new file mode 100644 index 000000000..dc6bea5df --- /dev/null +++ b/examples/helloworld_transcoding/priv/client.exs @@ -0,0 +1,9 @@ +{:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) + +{:ok, reply} = + channel + |> Helloworld.Greeter.Stub.say_hello(Helloworld.HelloRequest.new(name: "grpc-elixir")) + +# pass tuple `timeout: :infinity` as a second arg to stay in IEx debugging + +IO.inspect(reply) diff --git a/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto b/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto new file mode 100644 index 000000000..efdab3db6 --- /dev/null +++ b/examples/helloworld_transcoding/priv/protos/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/examples/helloworld_transcoding/priv/protos/google/api/http.proto b/examples/helloworld_transcoding/priv/protos/google/api/http.proto new file mode 100644 index 000000000..113fa936a --- /dev/null +++ b/examples/helloworld_transcoding/priv/protos/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/examples/helloworld_transcoding/priv/protos/helloworld.proto b/examples/helloworld_transcoding/priv/protos/helloworld.proto new file mode 100644 index 000000000..55c410057 --- /dev/null +++ b/examples/helloworld_transcoding/priv/protos/helloworld.proto @@ -0,0 +1,47 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.helloworld"; +option java_outer_classname = "HelloWorldProto"; +option objc_class_prefix = "HLW"; + +import "google/api/annotations.proto"; +import "google/protobuf/timestamp.proto"; + +package helloworld; + +// The greeting service definition. +service Greeter { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter/{name}" + }; + } + + rpc SayHelloFrom (HelloRequestFrom) returns (HelloReply) { + option (google.api.http) = { + post: "/v1/greeter" + body: "*" + }; + } +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// HelloRequestFrom! +message HelloRequestFrom { + // Name! + string name = 1; + // From! + string from = 2; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; + google.protobuf.Timestamp today = 2; +} diff --git a/examples/helloworld_transcoding/test/hello_world_test.exs b/examples/helloworld_transcoding/test/hello_world_test.exs new file mode 100644 index 000000000..962d07ac1 --- /dev/null +++ b/examples/helloworld_transcoding/test/hello_world_test.exs @@ -0,0 +1,16 @@ +defmodule HelloworldTest do + @moduledoc false + + use ExUnit.Case + + setup_all do + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) + [channel: channel] + end + + test "helloworld should be successful", %{channel: channel} do + req = Helloworld.HelloRequest.new(name: "grpc-elixir") + assert {:ok, %{message: msg, today: _}} = Helloworld.Greeter.Stub.say_hello(channel, req) + assert msg == "Hello grpc-elixir" + end +end diff --git a/examples/helloworld_transcoding/test/test_helper.exs b/examples/helloworld_transcoding/test/test_helper.exs new file mode 100644 index 000000000..869559e70 --- /dev/null +++ b/examples/helloworld_transcoding/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/examples/route_guide/README.md b/examples/route_guide/README.md index 7ab2d1a7b..5e9fa0df4 100644 --- a/examples/route_guide/README.md +++ b/examples/route_guide/README.md @@ -9,7 +9,7 @@ $ mix do deps.get, compile 2. Run the server ``` -$ mix grpc.server +$ mix run --no-halt ``` 2. Run the client @@ -35,7 +35,7 @@ Refer to [protobuf-elixir](https://github.com/tony612/protobuf-elixir#usage) for ## Authentication ``` -$ TLS=true mix grpc.server +$ TLS=true mix run --no-halt $ TLS=true mix run priv/client.exs ``` @@ -44,10 +44,3 @@ $ TLS=true mix run priv/client.exs * How to change log level? Check out `config/config.exs`, default to warn * Use local grpc-elixir? Uncomment `{:grpc, path: "../../"}` in `mix.exs` * Why is output format of `Feature` & `Point` different from normal map? Check out `lib/inspect.ex` -* How to start server when starting your application? - - Change the config to: - - ```elixir - config :grpc, start_server: true - ``` diff --git a/examples/route_guide/config/config.exs b/examples/route_guide/config/config.exs deleted file mode 100644 index f346b5d26..000000000 --- a/examples/route_guide/config/config.exs +++ /dev/null @@ -1,3 +0,0 @@ -use Mix.Config - -# config :grpc, start_server: true diff --git a/examples/route_guide/lib/app.ex b/examples/route_guide/lib/app.ex index 5410d767f..8cebbf319 100644 --- a/examples/route_guide/lib/app.ex +++ b/examples/route_guide/lib/app.ex @@ -5,11 +5,9 @@ defmodule Routeguide.App do @key_path Path.expand("./tls/server1.key", :code.priv_dir(:route_guide)) def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(RouteGuide.Data, []), - supervisor(GRPC.Server.Supervisor, [start_args()]) + RouteGuide.Data, + {GRPC.Server.Supervisor, start_args()} ] opts = [strategy: :one_for_one, name: Routeguide] @@ -17,12 +15,13 @@ defmodule Routeguide.App do end defp start_args do + opts = [endpoint: Routeguide.Endpoint, port: 10000, start_server: true] + if System.get_env("TLS") do cred = GRPC.Credential.new(ssl: [certfile: @cert_path, keyfile: @key_path]) - IO.inspect(cred) - {Routeguide.Endpoint, 10000, cred: cred} + Keyword.put(opts, :cred, cred) else - {Routeguide.Endpoint, 10000} + opts end end end diff --git a/examples/route_guide/lib/client.ex b/examples/route_guide/lib/client.ex index 150a751b4..5d33f4ba4 100644 --- a/examples/route_guide/lib/client.ex +++ b/examples/route_guide/lib/client.ex @@ -89,9 +89,7 @@ defmodule RouteGuide.Client do Enum.each(result_enum, fn {:ok, note} -> IO.puts( - "Got message #{note.message} at point(#{note.location.latitude}, #{ - note.location.longitude - })" + "Got message #{note.message} at point(#{note.location.latitude}, #{note.location.longitude})" ) end) end diff --git a/examples/route_guide/lib/data.ex b/examples/route_guide/lib/data.ex index 11cf03431..7d1fe5284 100644 --- a/examples/route_guide/lib/data.ex +++ b/examples/route_guide/lib/data.ex @@ -1,7 +1,9 @@ defmodule RouteGuide.Data do + use Agent + @json_path Path.expand("../priv/route_guide_db.json", __DIR__) - def start_link do + def start_link(_) do features = load_features() Agent.start_link(fn -> %{features: features, notes: %{}} end, name: __MODULE__) end @@ -20,13 +22,13 @@ defmodule RouteGuide.Data do defp load_features(path \\ @json_path) do data = File.read!(path) - items = Poison.Parser.parse!(data) + items = Jason.decode!(data) - Enum.map(items, fn %{"location" => location, "name" => name} -> + for %{"location" => location, "name" => name} <- items do point = Routeguide.Point.new(latitude: location["latitude"], longitude: location["longitude"]) Routeguide.Feature.new(name: name, location: point) - end) + end end end diff --git a/examples/route_guide/lib/endpoint.ex b/examples/route_guide/lib/endpoint.ex index 08ca7fc2f..5a25c582d 100644 --- a/examples/route_guide/lib/endpoint.ex +++ b/examples/route_guide/lib/endpoint.ex @@ -1,6 +1,6 @@ defmodule Routeguide.Endpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Server.Interceptors.Logger run Routeguide.RouteGuide.Server end diff --git a/examples/route_guide/lib/route_guide.pb.ex b/examples/route_guide/lib/route_guide.pb.ex index f4ce67429..f54465a45 100644 --- a/examples/route_guide/lib/route_guide.pb.ex +++ b/examples/route_guide/lib/route_guide.pb.ex @@ -1,81 +1,46 @@ defmodule Routeguide.Point do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - latitude: integer, - longitude: integer - } - defstruct [:latitude, :longitude] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :latitude, 1, optional: true, type: :int32 - field :longitude, 2, optional: true, type: :int32 + field :latitude, 1, type: :int32 + field :longitude, 2, type: :int32 end defmodule Routeguide.Rectangle do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - lo: Routeguide.Point.t(), - hi: Routeguide.Point.t() - } - defstruct [:lo, :hi] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :lo, 1, optional: true, type: Routeguide.Point - field :hi, 2, optional: true, type: Routeguide.Point + field :lo, 1, type: Routeguide.Point + field :hi, 2, type: Routeguide.Point end defmodule Routeguide.Feature do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - name: String.t(), - location: Routeguide.Point.t() - } - defstruct [:name, :location] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :name, 1, optional: true, type: :string - field :location, 2, optional: true, type: Routeguide.Point + field :name, 1, type: :string + field :location, 2, type: Routeguide.Point end defmodule Routeguide.RouteNote do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - location: Routeguide.Point.t(), - message: String.t() - } - defstruct [:location, :message] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :location, 1, optional: true, type: Routeguide.Point - field :message, 2, optional: true, type: :string + field :location, 1, type: Routeguide.Point + field :message, 2, type: :string end defmodule Routeguide.RouteSummary do - use Protobuf + @moduledoc false - @type t :: %__MODULE__{ - point_count: integer, - feature_count: integer, - distance: integer, - elapsed_time: integer - } - defstruct [:point_count, :feature_count, :distance, :elapsed_time] + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 - field :point_count, 1, optional: true, type: :int32 - field :feature_count, 2, optional: true, type: :int32 - field :distance, 3, optional: true, type: :int32 - field :elapsed_time, 4, optional: true, type: :int32 -end - -defmodule Routeguide.RouteGuide.Service do - use GRPC.Service, name: "routeguide.RouteGuide" - - rpc :GetFeature, Routeguide.Point, Routeguide.Feature - rpc :ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature) - rpc :RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary - rpc :RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) -end - -defmodule Routeguide.RouteGuide.Stub do - use GRPC.Stub, service: Routeguide.RouteGuide.Service + field :point_count, 1, type: :int32, json_name: "pointCount" + field :feature_count, 2, type: :int32, json_name: "featureCount" + field :distance, 3, type: :int32 + field :elapsed_time, 4, type: :int32, json_name: "elapsedTime" end diff --git a/examples/route_guide/lib/server.ex b/examples/route_guide/lib/server.ex index 3cf7e70fc..3e90b8bc8 100644 --- a/examples/route_guide/lib/server.ex +++ b/examples/route_guide/lib/server.ex @@ -13,7 +13,7 @@ defmodule Routeguide.RouteGuide.Server do end) end - @spec list_features(Routeguide.Rectangle.t(), GRPC.Server.Stream.t()) :: any + @spec list_features(Routeguide.Rectangle.t(), GRPC.Server.Stream.t()) :: any() def list_features(rect, stream) do features = Data.fetch_features() @@ -45,7 +45,7 @@ defmodule Routeguide.RouteGuide.Server do ) end - @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: any + @spec record_route(Enumerable.t(), GRPC.Server.Stream.t()) :: any() def route_chat(req_enum, stream) do notes = Enum.reduce(req_enum, Data.fetch_notes(), fn note, notes -> diff --git a/examples/route_guide/mix.exs b/examples/route_guide/mix.exs index 812a09d8f..504e82676 100644 --- a/examples/route_guide/mix.exs +++ b/examples/route_guide/mix.exs @@ -2,20 +2,21 @@ defmodule RouteGuide.Mixfile do use Mix.Project def project do - [app: :route_guide, - version: "0.1.0", - elixir: "~> 1.3", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps()] + [ + app: :route_guide, + version: "0.1.0", + elixir: "~> 1.11", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] end # Configuration for the OTP application # # Type "mix help compile.app" for more information def application do - [mod: {Routeguide.App, []}, - applications: [:logger, :grpc, :poison]] + [mod: {Routeguide.App, []}, applications: [:logger, :grpc, :protobuf, :jason]] end # Dependencies can be Hex packages: @@ -30,9 +31,8 @@ defmodule RouteGuide.Mixfile do defp deps do [ {:grpc, path: "../../"}, - {:poison, "~> 3.0"}, - {:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}, - {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + {:protobuf, "~> 0.14"}, + {:jason, "~> 1.2"} ] end end diff --git a/examples/route_guide/mix.lock b/examples/route_guide/mix.lock index 5205d247f..78e1d6e3d 100644 --- a/examples/route_guide/mix.lock +++ b/examples/route_guide/mix.lock @@ -1,9 +1,11 @@ %{ - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :grpc_cowlib, "2.8.1", "ddaf77f3b89bd8e6c76df67b28a4b069688eef91c0c497a246cf9bfcdf87f7d3", [:rebar3], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], []}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], []}, - "protobuf": {:hex, :protobuf, "0.5.0", "ec9857903f8c49cf01ad5f1340956e19ebe0ef05e225b15b442f33b13031ce08", [:mix], []}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, + "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, + "gun": {:hex, :gun, "2.1.0", "b4e4cbbf3026d21981c447e9e7ca856766046eff693720ba43114d7f5de36e87", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "52fc7fc246bfc3b00e01aea1c2854c70a366348574ab50c57dfe796d24a0101d"}, + "hpax": {:hex, :hpax, "1.0.2", "762df951b0c399ff67cc57c3995ec3cf46d696e41f0bba17da0518d94acd4aac", [:mix], [], "hexpm", "2f09b4c1074e0abd846747329eaa26d535be0eb3d189fa69d812bfb8bfefd32f"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, } diff --git a/examples/route_guide/priv/client.exs b/examples/route_guide/priv/client.exs index d3c3178e8..5545ce9f7 100644 --- a/examples/route_guide/priv/client.exs +++ b/examples/route_guide/priv/client.exs @@ -1,9 +1,10 @@ -opts = [interceptors: [GRPC.Logger.Client]] +opts = [interceptors: [GRPC.Client.Interceptors.Logger]] + opts = if System.get_env("TLS") do ca_path = Path.expand("./tls/ca.pem", :code.priv_dir(:route_guide)) cred = GRPC.Credential.new(ssl: [cacertfile: ca_path]) - [{:cred, cred}|opts] + [{:cred, cred} | opts] else opts end diff --git a/examples/route_guide/priv/route_guide.proto b/examples/route_guide/priv/protos/route_guide.proto similarity index 100% rename from examples/route_guide/priv/route_guide.proto rename to examples/route_guide/priv/protos/route_guide.proto diff --git a/guides/pooling.md b/guides/pooling.md new file mode 100644 index 000000000..33a6e031a --- /dev/null +++ b/guides/pooling.md @@ -0,0 +1,9 @@ +# Managing HTTP/2 Connections Efficiently + +When managing large numbers of gRPC HTTP/2 connections, you may benefit from pooling of some sort. + +Currently `elixir-grpc` does not offer pooling functionality, but [here is an excellent guide on using Elixir's `Registry` module to create your own resource pools](https://andrealeopardi.com/posts/process-pools-with-elixirs-registry/). + +It's also worth noting that if using the `Mint` adapter for HTTP/2, you can choose which connection to use by checking the value of each connection's open request count with `open_request_count/1` . + +If you'd prefer to use an existing pool implementation, [check out `conn_grpc` on Hex](https://hexdocs.pm/conn_grpc/ConnGRPC.Pool.html)! diff --git a/interop/config/config.exs b/interop/config/config.exs index 3fe942a96..63787d4b3 100644 --- a/interop/config/config.exs +++ b/interop/config/config.exs @@ -1,12 +1,3 @@ -use Mix.Config +import Config -config :prometheus, GRPCPrometheus.ServerInterceptor, - latency: :histogram - -config :prometheus, GRPCPrometheus.ClientInterceptor, - latency: :histogram - -# config :grpc, start_server: true - -# config :logger, level: :debug -config :logger, level: :warn +config :logger, level: :warning diff --git a/interop/lib/grpc_testing/empty.pb.ex b/interop/lib/grpc_testing/empty.pb.ex index 0208e2662..77ee57fdf 100644 --- a/interop/lib/grpc_testing/empty.pb.ex +++ b/interop/lib/grpc_testing/empty.pb.ex @@ -1,7 +1,4 @@ defmodule Grpc.Testing.Empty do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{} - defstruct [] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 end diff --git a/interop/lib/grpc_testing/messages.pb.ex b/interop/lib/grpc_testing/messages.pb.ex index aec33378b..6a9d03e82 100644 --- a/interop/lib/grpc_testing/messages.pb.ex +++ b/interop/lib/grpc_testing/messages.pb.ex @@ -1,202 +1,283 @@ defmodule Grpc.Testing.PayloadType do @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - - @type t :: integer | :COMPRESSABLE + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :COMPRESSABLE, 0 end +defmodule Grpc.Testing.GrpclbRouteType do + @moduledoc false + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 -defmodule Grpc.Testing.BoolValue do + field :GRPCLB_ROUTE_TYPE_UNKNOWN, 0 + field :GRPCLB_ROUTE_TYPE_FALLBACK, 1 + field :GRPCLB_ROUTE_TYPE_BACKEND, 2 +end +defmodule Grpc.Testing.ClientConfigureRequest.RpcType do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, enum: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - value: boolean - } - defstruct [:value] + field :EMPTY_CALL, 0 + field :UNARY_CALL, 1 +end +defmodule Grpc.Testing.BoolValue do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :value, 1, type: :bool end - defmodule Grpc.Testing.Payload do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - type: Grpc.Testing.PayloadType.t(), - body: binary - } - defstruct [:type, :body] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :type, 1, type: Grpc.Testing.PayloadType, enum: true field :body, 2, type: :bytes end - defmodule Grpc.Testing.EchoStatus do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - code: integer, - message: String.t() - } - defstruct [:code, :message] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :code, 1, type: :int32 field :message, 2, type: :string end - defmodule Grpc.Testing.SimpleRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - response_type: Grpc.Testing.PayloadType.t(), - response_size: integer, - payload: Grpc.Testing.Payload.t() | nil, - fill_username: boolean, - fill_oauth_scope: boolean, - response_compressed: Grpc.Testing.BoolValue.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil, - fill_server_id: boolean - } - defstruct [ - :response_type, - :response_size, - :payload, - :fill_username, - :fill_oauth_scope, - :response_compressed, - :response_status, - :expect_compressed, - :fill_server_id - ] - - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_size, 2, type: :int32 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true + field :response_size, 2, type: :int32, json_name: "responseSize" field :payload, 3, type: Grpc.Testing.Payload - field :fill_username, 4, type: :bool - field :fill_oauth_scope, 5, type: :bool - field :response_compressed, 6, type: Grpc.Testing.BoolValue - field :response_status, 7, type: Grpc.Testing.EchoStatus - field :expect_compressed, 8, type: Grpc.Testing.BoolValue - field :fill_server_id, 9, type: :bool + field :fill_username, 4, type: :bool, json_name: "fillUsername" + field :fill_oauth_scope, 5, type: :bool, json_name: "fillOauthScope" + field :response_compressed, 6, type: Grpc.Testing.BoolValue, json_name: "responseCompressed" + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" + field :expect_compressed, 8, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" + field :fill_server_id, 9, type: :bool, json_name: "fillServerId" + field :fill_grpclb_route_type, 10, type: :bool, json_name: "fillGrpclbRouteType" end - defmodule Grpc.Testing.SimpleResponse do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - username: String.t(), - oauth_scope: String.t(), - server_id: String.t() - } - defstruct [:payload, :username, :oauth_scope, :server_id] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload field :username, 2, type: :string - field :oauth_scope, 3, type: :string - field :server_id, 4, type: :string -end + field :oauth_scope, 3, type: :string, json_name: "oauthScope" + field :server_id, 4, type: :string, json_name: "serverId" + + field :grpclb_route_type, 5, + type: Grpc.Testing.GrpclbRouteType, + json_name: "grpclbRouteType", + enum: true + field :hostname, 6, type: :string +end defmodule Grpc.Testing.StreamingInputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil, - expect_compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:payload, :expect_compressed] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload - field :expect_compressed, 2, type: Grpc.Testing.BoolValue + field :expect_compressed, 2, type: Grpc.Testing.BoolValue, json_name: "expectCompressed" end - defmodule Grpc.Testing.StreamingInputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - aggregated_payload_size: integer - } - defstruct [:aggregated_payload_size] - - field :aggregated_payload_size, 1, type: :int32 + field :aggregated_payload_size, 1, type: :int32, json_name: "aggregatedPayloadSize" end - defmodule Grpc.Testing.ResponseParameters do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - size: integer, - interval_us: integer, - compressed: Grpc.Testing.BoolValue.t() | nil - } - defstruct [:size, :interval_us, :compressed] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :size, 1, type: :int32 - field :interval_us, 2, type: :int32 + field :interval_us, 2, type: :int32, json_name: "intervalUs" field :compressed, 3, type: Grpc.Testing.BoolValue end - defmodule Grpc.Testing.StreamingOutputCallRequest do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :response_type, 1, type: Grpc.Testing.PayloadType, json_name: "responseType", enum: true - @type t :: %__MODULE__{ - response_type: Grpc.Testing.PayloadType.t(), - response_parameters: [Grpc.Testing.ResponseParameters.t()], - payload: Grpc.Testing.Payload.t() | nil, - response_status: Grpc.Testing.EchoStatus.t() | nil - } - defstruct [:response_type, :response_parameters, :payload, :response_status] + field :response_parameters, 2, + repeated: true, + type: Grpc.Testing.ResponseParameters, + json_name: "responseParameters" - field :response_type, 1, type: Grpc.Testing.PayloadType, enum: true - field :response_parameters, 2, repeated: true, type: Grpc.Testing.ResponseParameters field :payload, 3, type: Grpc.Testing.Payload - field :response_status, 7, type: Grpc.Testing.EchoStatus + field :response_status, 7, type: Grpc.Testing.EchoStatus, json_name: "responseStatus" end - defmodule Grpc.Testing.StreamingOutputCallResponse do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - payload: Grpc.Testing.Payload.t() | nil - } - defstruct [:payload] + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 field :payload, 1, type: Grpc.Testing.Payload end - defmodule Grpc.Testing.ReconnectParams do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - max_reconnect_backoff_ms: integer - } - defstruct [:max_reconnect_backoff_ms] + field :max_reconnect_backoff_ms, 1, type: :int32, json_name: "maxReconnectBackoffMs" +end +defmodule Grpc.Testing.ReconnectInfo do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :max_reconnect_backoff_ms, 1, type: :int32 + field :passed, 1, type: :bool + field :backoff_ms, 2, repeated: true, type: :int32, json_name: "backoffMs" end +defmodule Grpc.Testing.LoadBalancerStatsRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 -defmodule Grpc.Testing.ReconnectInfo do + field :num_rpcs, 1, type: :int32, json_name: "numRpcs" + field :timeout_sec, 2, type: :int32, json_name: "timeoutSec" +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer.RpcsByPeerEntry do @moduledoc false - use Protobuf, syntax: :proto3 + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - @type t :: %__MODULE__{ - passed: boolean, - backoff_ms: [integer] - } - defstruct [:passed, :backoff_ms] + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 - field :passed, 1, type: :bool - field :backoff_ms, 2, repeated: true, type: :int32 + field :rpcs_by_peer, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer.RpcsByPeerEntry, + json_name: "rpcsByPeer", + map: true +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeerEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerStatsResponse.RpcsByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeer +end +defmodule Grpc.Testing.LoadBalancerStatsResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :rpcs_by_peer, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByPeerEntry, + json_name: "rpcsByPeer", + map: true + + field :num_failures, 2, type: :int32, json_name: "numFailures" + + field :rpcs_by_method, 3, + repeated: true, + type: Grpc.Testing.LoadBalancerStatsResponse.RpcsByMethodEntry, + json_name: "rpcsByMethod", + map: true +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsStartedByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsSucceededByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsFailedByMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats.ResultEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :int32 + field :value, 2, type: :int32 +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :rpcs_started, 1, type: :int32, json_name: "rpcsStarted" + + field :result, 2, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats.ResultEntry, + map: true +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse.StatsPerMethodEntry do + @moduledoc false + use Protobuf, map: true, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :key, 1, type: :string + field :value, 2, type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.MethodStats +end +defmodule Grpc.Testing.LoadBalancerAccumulatedStatsResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :num_rpcs_started_by_method, 1, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsStartedByMethodEntry, + json_name: "numRpcsStartedByMethod", + map: true, + deprecated: true + + field :num_rpcs_succeeded_by_method, 2, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsSucceededByMethodEntry, + json_name: "numRpcsSucceededByMethod", + map: true, + deprecated: true + + field :num_rpcs_failed_by_method, 3, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.NumRpcsFailedByMethodEntry, + json_name: "numRpcsFailedByMethod", + map: true, + deprecated: true + + field :stats_per_method, 4, + repeated: true, + type: Grpc.Testing.LoadBalancerAccumulatedStatsResponse.StatsPerMethodEntry, + json_name: "statsPerMethod", + map: true +end +defmodule Grpc.Testing.ClientConfigureRequest.Metadata do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :type, 1, type: Grpc.Testing.ClientConfigureRequest.RpcType, enum: true + field :key, 2, type: :string + field :value, 3, type: :string +end +defmodule Grpc.Testing.ClientConfigureRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 + + field :types, 1, repeated: true, type: Grpc.Testing.ClientConfigureRequest.RpcType, enum: true + field :metadata, 2, repeated: true, type: Grpc.Testing.ClientConfigureRequest.Metadata + field :timeout_sec, 3, type: :int32, json_name: "timeoutSec" +end +defmodule Grpc.Testing.ClientConfigureResponse do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.10.0", syntax: :proto3 end diff --git a/interop/lib/grpc_testing/test.pb.ex b/interop/lib/grpc_testing/test.pb.ex index 963a45c56..a1a231cd4 100644 --- a/interop/lib/grpc_testing/test.pb.ex +++ b/interop/lib/grpc_testing/test.pb.ex @@ -1,9 +1,11 @@ defmodule Grpc.Testing.TestService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.TestService" + use GRPC.Service, name: "grpc.testing.TestService", protoc_gen_elixir_version: "0.10.0" rpc :EmptyCall, Grpc.Testing.Empty, Grpc.Testing.Empty + rpc :UnaryCall, Grpc.Testing.SimpleRequest, Grpc.Testing.SimpleResponse + rpc :CacheableUnaryCall, Grpc.Testing.SimpleRequest, Grpc.Testing.SimpleResponse rpc :StreamingOutputCall, @@ -29,10 +31,9 @@ defmodule Grpc.Testing.TestService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.TestService.Service end - defmodule Grpc.Testing.UnimplementedService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.UnimplementedService" + use GRPC.Service, name: "grpc.testing.UnimplementedService", protoc_gen_elixir_version: "0.10.0" rpc :UnimplementedCall, Grpc.Testing.Empty, Grpc.Testing.Empty end @@ -41,12 +42,12 @@ defmodule Grpc.Testing.UnimplementedService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.UnimplementedService.Service end - defmodule Grpc.Testing.ReconnectService.Service do @moduledoc false - use GRPC.Service, name: "grpc.testing.ReconnectService" + use GRPC.Service, name: "grpc.testing.ReconnectService", protoc_gen_elixir_version: "0.10.0" rpc :Start, Grpc.Testing.ReconnectParams, Grpc.Testing.Empty + rpc :Stop, Grpc.Testing.Empty, Grpc.Testing.ReconnectInfo end @@ -54,3 +55,50 @@ defmodule Grpc.Testing.ReconnectService.Stub do @moduledoc false use GRPC.Stub, service: Grpc.Testing.ReconnectService.Service end +defmodule Grpc.Testing.LoadBalancerStatsService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.LoadBalancerStatsService", + protoc_gen_elixir_version: "0.10.0" + + rpc :GetClientStats, + Grpc.Testing.LoadBalancerStatsRequest, + Grpc.Testing.LoadBalancerStatsResponse + + rpc :GetClientAccumulatedStats, + Grpc.Testing.LoadBalancerAccumulatedStatsRequest, + Grpc.Testing.LoadBalancerAccumulatedStatsResponse +end + +defmodule Grpc.Testing.LoadBalancerStatsService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.LoadBalancerStatsService.Service +end +defmodule Grpc.Testing.XdsUpdateHealthService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.XdsUpdateHealthService", + protoc_gen_elixir_version: "0.10.0" + + rpc :SetServing, Grpc.Testing.Empty, Grpc.Testing.Empty + + rpc :SetNotServing, Grpc.Testing.Empty, Grpc.Testing.Empty +end + +defmodule Grpc.Testing.XdsUpdateHealthService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.XdsUpdateHealthService.Service +end +defmodule Grpc.Testing.XdsUpdateClientConfigureService.Service do + @moduledoc false + use GRPC.Service, + name: "grpc.testing.XdsUpdateClientConfigureService", + protoc_gen_elixir_version: "0.10.0" + + rpc :Configure, Grpc.Testing.ClientConfigureRequest, Grpc.Testing.ClientConfigureResponse +end + +defmodule Grpc.Testing.XdsUpdateClientConfigureService.Stub do + @moduledoc false + use GRPC.Stub, service: Grpc.Testing.XdsUpdateClientConfigureService.Service +end diff --git a/interop/lib/interop/app.ex b/interop/lib/interop/app.ex index 22857fdef..1981f7c8f 100644 --- a/interop/lib/interop/app.ex +++ b/interop/lib/interop/app.ex @@ -2,16 +2,7 @@ defmodule Interop.App do use Application def start(_type, _args) do - import Supervisor.Spec - - children = [ - supervisor(GRPC.Server.Supervisor, [{Interop.Endpoint, 10000}]) - ] - - GRPCPrometheus.ServerInterceptor.setup() - GRPCPrometheus.ClientInterceptor.setup() - :prometheus_httpd.start() - Interop.ServerInterceptor.Statix.connect() + children = [{GRPC.Server.Supervisor, endpoint: Interop.Endpoint, port: 10000}] opts = [strategy: :one_for_one, name: __MODULE__] Supervisor.start_link(children, opts) diff --git a/interop/lib/interop/client.ex b/interop/lib/interop/client.ex index dfa9e041c..760137366 100644 --- a/interop/lib/interop/client.ex +++ b/interop/lib/interop/client.ex @@ -1,13 +1,20 @@ defmodule Interop.Client do + import ExUnit.Assertions, only: [refute: 1] + require Logger + + # To better understand the behavior of streams used in this module + # we suggest you to check the documentation for `GRPC.Stub.recv/2` + # there is some unusual behavior that can be observed. + def connect(host, port, opts \\ []) do {:ok, ch} = GRPC.Stub.connect(host, port, opts) ch end def empty_unary!(ch) do - IO.puts("Run empty_unary!") + Logger.info("Run empty_unary!") empty = Grpc.Testing.Empty.new() {:ok, ^empty} = Grpc.Testing.TestService.Stub.empty_call(ch, empty) end @@ -17,21 +24,21 @@ defmodule Interop.Client do end def large_unary!(ch) do - IO.puts("Run large_unary!") + Logger.info("Run large_unary!") req = Grpc.Testing.SimpleRequest.new(response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) {:ok, ^reply} = Grpc.Testing.TestService.Stub.unary_call(ch, req) end def large_unary2!(ch) do - IO.puts("Run large_unary2!") + Logger.info("Run large_unary2!") req = Grpc.Testing.SimpleRequest.new(response_size: 1024*1024*8, payload: payload(1024*1024*8)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(1024*1024*8)) {:ok, ^reply} = Grpc.Testing.TestService.Stub.unary_call(ch, req) end def client_compressed_unary!(ch) do - IO.puts("Run client_compressed_unary!") + Logger.info("Run client_compressed_unary!") # "Client calls UnaryCall with the feature probe, an uncompressed message" is not supported req = Grpc.Testing.SimpleRequest.new(expect_compressed: %{value: true}, response_size: 314_159, payload: payload(271_828)) @@ -44,7 +51,7 @@ defmodule Interop.Client do end def server_compressed_unary!(ch) do - IO.puts("Run server_compressed_unary!") + Logger.info("Run server_compressed_unary!") req = Grpc.Testing.SimpleRequest.new(response_compressed: %{value: true}, response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) @@ -57,7 +64,7 @@ defmodule Interop.Client do end def client_streaming!(ch) do - IO.puts("Run client_streaming!") + Logger.info("Run client_streaming!") stream = ch @@ -79,7 +86,7 @@ defmodule Interop.Client do end def client_compressed_streaming!(ch) do - IO.puts("Run client_compressed_streaming!") + Logger.info("Run client_compressed_streaming!") # INVALID_ARGUMENT testing is not supported @@ -97,20 +104,17 @@ defmodule Interop.Client do end def server_streaming!(ch) do - IO.puts("Run server_streaming!") + Logger.info("Run server_streaming!") params = Enum.map([31415, 9, 2653, 58979], &res_param(&1)) req = Grpc.Testing.StreamingOutputCallRequest.new(response_parameters: params) {:ok, res_enum} = ch |> Grpc.Testing.TestService.Stub.streaming_output_call(req) - result = Enum.map([31415, 9, 2653, 58979], &String.duplicate(<<0>>, &1)) + result = Enum.map([9, 2653, 31415, 58979], &String.duplicate(<<0>>, &1)) - ^result = - Enum.map(res_enum, fn {:ok, res} -> - res.payload.body - end) + ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end def server_compressed_streaming!(ch) do - IO.puts("Run server_compressed_streaming!") + Logger.info("Run server_compressed_streaming!") req = Grpc.Testing.StreamingOutputCallRequest.new(response_parameters: [ %{compressed: %{value: true}, size: 31415}, @@ -120,14 +124,11 @@ defmodule Interop.Client do {:ok, res_enum} = ch |> Grpc.Testing.TestService.Stub.streaming_output_call(req) result = Enum.map([31415, 92653], &String.duplicate(<<0>>, &1)) - ^result = - Enum.map(res_enum, fn {:ok, res} -> - res.payload.body - end) + ^result = res_enum |> Enum.map(fn {:ok, res} -> res.payload.body end) |> Enum.sort() end def ping_pong!(ch) do - IO.puts("Run ping_pong!") + Logger.info("Run ping_pong!") stream = Grpc.Testing.TestService.Stub.full_duplex_call(ch) req = fn size1, size2 -> @@ -141,22 +142,20 @@ defmodule Interop.Client do {:ok, res_enum} = GRPC.Stub.recv(stream) reply = String.duplicate(<<0>>, 31415) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, %{payload: %{body: ^reply}}} = Enum.at(res_enum, 0) Enum.each([{9, 8}, {2653, 1828}, {58979, 45904}], fn {res, payload} -> GRPC.Stub.send_request(stream, req.(res, payload)) reply = String.duplicate(<<0>>, res) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, %{payload: %{body: ^reply}}} = Enum.at(res_enum, 0) end) GRPC.Stub.end_stream(stream) end def empty_stream!(ch) do - IO.puts("Run empty_stream!") + Logger.info("Run empty_stream!") {:ok, res_enum} = ch @@ -168,7 +167,7 @@ defmodule Interop.Client do end def custom_metadata!(ch) do - IO.puts("Run custom_metadata!") + Logger.info("Run custom_metadata!") # UnaryCall req = Grpc.Testing.SimpleRequest.new(response_size: 314_159, payload: payload(271_828)) reply = Grpc.Testing.SimpleResponse.new(payload: payload(314_159)) @@ -189,23 +188,36 @@ defmodule Interop.Client do payload: payload(271_828) ) - {:ok, res_enum, %{headers: new_headers}} = + {headers, data, trailers} = ch |> Grpc.Testing.TestService.Stub.full_duplex_call(metadata: metadata) |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv(return_headers: true) + |> process_full_duplex_response() reply = String.duplicate(<<0>>, 314_159) - {:ok, %{payload: %{body: ^reply}}} = - Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + %{payload: %{body: ^reply}} = data - {:trailers, new_trailers} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() - validate_headers!(new_headers, new_trailers) + validate_headers!(headers, trailers) + end + + defp process_full_duplex_response({:ok, res_enum, %{headers: new_headers}}) do + {:ok, data} = Enum.at(res_enum, 0) + {:trailers, new_trailers} = Enum.at(res_enum, 0) + {new_headers, data, new_trailers} + end + + + defp process_full_duplex_response({:ok, res_enum}) do + {:headers, headers} = Enum.at(res_enum, 0) + {:ok, data} = Enum.at(res_enum, 0) + {:trailers, trailers} = Enum.at(res_enum, 0) + {headers, data, trailers} end def status_code_and_message!(ch) do - IO.puts("Run status_code_and_message!") + Logger.info("Run status_code_and_message!") code = 2 msg = "test status message" @@ -224,10 +236,14 @@ defmodule Interop.Client do |> Grpc.Testing.TestService.Stub.full_duplex_call() |> GRPC.Stub.send_request(req, end_stream: true) |> GRPC.Stub.recv() + |> case do + {:ok, stream} -> Enum.at(stream, 0) + error -> error + end end def unimplemented_service!(ch) do - IO.puts("Run unimplemented_service!") + Logger.info("Run unimplemented_service!") req = Grpc.Testing.Empty.new() {:error, %GRPC.RPCError{status: 12}} = @@ -235,7 +251,7 @@ defmodule Interop.Client do end def cancel_after_begin!(ch) do - IO.puts("Run cancel_after_begin!") + Logger.info("Run cancel_after_begin!") stream = Grpc.Testing.TestService.Stub.streaming_input_call(ch) stream = GRPC.Stub.cancel(stream) error = GRPC.RPCError.exception(1, "The operation was cancelled") @@ -243,7 +259,7 @@ defmodule Interop.Client do end def cancel_after_first_response!(ch) do - IO.puts("Run cancel_after_first_response!") + Logger.info("Run cancel_after_first_response!") req = Grpc.Testing.StreamingOutputCallRequest.new( @@ -258,13 +274,13 @@ defmodule Interop.Client do |> GRPC.Stub.send_request(req) |> GRPC.Stub.recv() - {:ok, _} = Stream.take(res_enum, 1) |> Enum.to_list() |> List.first() + {:ok, _} = Enum.at(res_enum, 0) stream = GRPC.Stub.cancel(stream) {:error, %GRPC.RPCError{status: 1}} = GRPC.Stub.recv(stream) end def timeout_on_sleeping_server!(ch) do - IO.puts("Run timeout_on_sleeping_server!") + Logger.info("Run timeout_on_sleeping_server!") req = Grpc.Testing.StreamingOutputCallRequest.new( diff --git a/interop/lib/interop/endpoint.ex b/interop/lib/interop/endpoint.ex index c979ac156..f58cc9b8b 100644 --- a/interop/lib/interop/endpoint.ex +++ b/interop/lib/interop/endpoint.ex @@ -1,9 +1,5 @@ defmodule Interop.Endpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server - intercept GRPCPrometheus.ServerInterceptor - intercept Interop.ServerInterceptor - run Interop.Server end diff --git a/interop/lib/interop/stats_interceptor.ex b/interop/lib/interop/stats_interceptor.ex deleted file mode 100644 index 9813f54be..000000000 --- a/interop/lib/interop/stats_interceptor.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Interop.ServerInterceptor.Statix do - use Statix -end - -defmodule Interop.ServerInterceptor do - use GRPCStatsd.ServerInterceptor, statsd_module: Interop.ServerInterceptor.Statix -end diff --git a/interop/mix.exs b/interop/mix.exs index dd3dff6a1..611d6a8da 100644 --- a/interop/mix.exs +++ b/interop/mix.exs @@ -23,13 +23,10 @@ defmodule Interop.MixProject do defp deps do [ {:grpc, path: "..", override: true}, - {:cowlib, "~> 2.9.0", override: true}, - {:grpc_prometheus, ">= 0.1.0"}, + {:protobuf, "~> 0.14"}, {:grpc_statsd, "~> 0.1.0"}, {:statix, ">= 1.2.1"}, {:extrace, "~> 0.2"}, - {:prometheus, "~> 4.0", override: true}, - {:prometheus_httpd, "~> 2.0"} ] end end diff --git a/interop/mix.lock b/interop/mix.lock index b04bcec6c..7e2e46ed5 100644 --- a/interop/mix.lock +++ b/interop/mix.lock @@ -1,17 +1,15 @@ %{ - "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, - "cowlib": {:hex, :cowlib, "2.9.0", "8365736a2ada74d5e8640c9b03efff15aceffcf2c7cba2e5ffd0c549f54bf0da", [:rebar3], [], "hexpm", "e8a93bbdf5c4f3d63fbb0cae422de2b58227955bebad5fed978f7a83b0ca4c89"}, - "extrace": {:hex, :extrace, "0.2.1", "e234f1f64df8c989771b7b5d047a3412f10512c0e3d414fc7eb0e8fc633779f8", [:mix], [{:recon, "~> 2.5", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "1b2d9fc4bacc208d5aaa97f61e7c47b66ff4dfc155e9b95647e68ca316ab3981"}, + "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, + "extrace": {:hex, :extrace, "0.5.0", "4ee5419fbc3820c4592daebe0f8527001aa623578d9a725d8ae521315fce0277", [:mix], [{:recon, "~> 2.5", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "2a3ab7fa0701949efee1034293fa0b0e65926ffe256ccd6d0e10dd8a9406cd02"}, "grpc": {:git, "https://github.com/elixir-grpc/grpc.git", "21422839798e49bf6d29327fab0a7add51becedd", []}, - "grpc_prometheus": {:hex, :grpc_prometheus, "0.1.0", "a2f45ca83018c4ae59e4c293b7455634ac09e38c36cba7cc1fb8affdf462a6d5", [:mix], [{:grpc, ">= 0.0.0", [hex: :grpc, repo: "hexpm", optional: true]}, {:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm", "8b9ab3098657e7daec0b3edc78e1d02418bc0871618d8ca89b51b74a8086bb71"}, "grpc_statsd": {:hex, :grpc_statsd, "0.1.0", "a95ae388188486043f92a3c5091c143f5a646d6af80c9da5ee616546c4d8f5ff", [:mix], [{:grpc, ">= 0.0.0", [hex: :grpc, repo: "hexpm", optional: true]}, {:statix, ">= 0.0.0", [hex: :statix, repo: "hexpm", optional: true]}], "hexpm", "de0c05db313c7b3ffeff345855d173fd82fec3de16591a126b673f7f698d9e74"}, - "gun": {:hex, :grpc_gun, "2.0.0", "f99678a2ab975e74372a756c86ec30a8384d3ac8a8b86c7ed6243ef4e61d2729", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "03dbbca1a9c604a0267a40ea1d69986225091acb822de0b2dbea21d5815e410b"}, - "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm", "b479a33d4aa4ba7909186e29bb6c1240254e0047a8e2a9f88463f50c0089370e"}, - "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "9fd13404a48437e044b288b41f76e64acd9735fb8b0e3809f494811dfa66d0fb"}, - "prometheus_httpd": {:hex, :prometheus_httpd, "2.1.11", "f616ed9b85b536b195d94104063025a91f904a4cfc20255363f49a197d96c896", [:rebar3], [{:accept, "~> 0.3", [hex: :accept, repo: "hexpm", optional: false]}, {:prometheus, "~> 4.2", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm", "0bbe831452cfdf9588538eb2f570b26f30c348adae5e95a7d87f35a5910bcf92"}, - "protobuf": {:hex, :protobuf, "0.7.1", "7d1b9f7d9ecb32eccd96b0c58572de4d1c09e9e3bc414e4cb15c2dce7013f195", [:mix], [], "hexpm", "6eff7a5287963719521c82e5d5b4583fd1d7cdd89ad129f0ea7d503a50a4d13f"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, - "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, - "statix": {:hex, :statix, "1.2.1", "4f23c8cc2477ea0de89fed5e34f08c54b0d28b838f7b8f26613155f2221bb31e", [:mix], [], "hexpm", "7f988988fddcce19ae376bb8e47aa5ea5dabf8d4ba78d34d1ae61eb537daf72e"}, + "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "mint": {:hex, :mint, "1.5.1", "8db5239e56738552d85af398798c80648db0e90f343c8469f6c6d8898944fb6f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4a63e1e76a7c3956abd2c72f370a0d0aecddc3976dea5c27eccbecfa5e7d5b1e"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "recon": {:hex, :recon, "2.5.5", "c108a4c406fa301a529151a3bb53158cadc4064ec0c5f99b03ddb8c0e4281bdf", [:mix, :rebar3], [], "hexpm", "632a6f447df7ccc1a4a10bdcfce71514412b16660fe59deca0fcf0aa3c054404"}, + "statix": {:hex, :statix, "1.4.0", "c822abd1e60e62828e8460e932515d0717aa3c089b44cc3f795d43b94570b3a8", [:mix], [], "hexpm", "507373cc80925a9b6856cb14ba17f6125552434314f6613c907d295a09d1a375"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/interop/script/run.exs b/interop/script/run.exs index 0728d6e7b..554d641b5 100644 --- a/interop/script/run.exs +++ b/interop/script/run.exs @@ -1,58 +1,60 @@ -{options, _, _} = OptionParser.parse(System.argv(), strict: [rounds: :integer, concurrency: :integer, port: :integer]) -rounds = Keyword.get(options, :rounds, 100) -concurrency = Keyword.get(options, :concurrency, 10) -port = Keyword.get(options, :port, 0) +{options, _, _} = OptionParser.parse(System.argv(), strict: [rounds: :integer, concurrency: :integer, port: :integer, level: :string]) +rounds = Keyword.get(options, :rounds) || 20 +max_concurrency = System.schedulers_online() +concurrency = Keyword.get(options, :concurrency) || max_concurrency +port = Keyword.get(options, :port) || 0 +level = Keyword.get(options, :level) || "warning" +level = String.to_existing_atom(level) -IO.puts "Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}" -IO.puts "" +require Logger +Logger.configure(level: level) + +Logger.info("Rounds: #{rounds}; concurrency: #{concurrency}; port: #{port}") + +alias GRPC.Client.Adapters.Gun +alias GRPC.Client.Adapters.Mint alias Interop.Client + {:ok, _pid, port} = GRPC.Server.start_endpoint(Interop.Endpoint, port) -stream = Task.async_stream(1..concurrency, fn cli -> - ch = Client.connect("127.0.0.1", port, interceptors: [GRPCPrometheus.ClientInterceptor, GRPC.Logger.Client]) - run = fn(i) -> - IO.puts("Client##{cli}, Round #{i}") - Client.empty_unary!(ch) - Client.cacheable_unary!(ch) - Client.large_unary!(ch) - Client.large_unary2!(ch) - Client.client_compressed_unary!(ch) - Client.server_compressed_unary!(ch) - Client.client_streaming!(ch) - Client.client_compressed_streaming!(ch) - Client.server_streaming!(ch) - Client.server_compressed_streaming!(ch) - Client.ping_pong!(ch) - Client.empty_stream!(ch) - Client.custom_metadata!(ch) - Client.status_code_and_message!(ch) - Client.unimplemented_service!(ch) - Client.cancel_after_begin!(ch) - Client.cancel_after_first_response!(ch) - Client.timeout_on_sleeping_server!(ch) +defmodule InteropTestRunner do + def run(_cli, adapter, port, rounds) do + opts = [interceptors: [GRPC.Client.Interceptors.Logger], adapter: adapter] + ch = Client.connect("127.0.0.1", port, opts) + + for _ <- 1..rounds do + Client.empty_unary!(ch) + Client.cacheable_unary!(ch) + Client.large_unary!(ch) + Client.large_unary2!(ch) + Client.client_compressed_unary!(ch) + Client.server_compressed_unary!(ch) + Client.client_streaming!(ch) + Client.client_compressed_streaming!(ch) + Client.server_streaming!(ch) + Client.server_compressed_streaming!(ch) + Client.ping_pong!(ch) + Client.empty_stream!(ch) + Client.custom_metadata!(ch) + Client.status_code_and_message!(ch) + Client.unimplemented_service!(ch) + Client.cancel_after_begin!(ch) + Client.cancel_after_first_response!(ch) + Client.timeout_on_sleeping_server!(ch) + end + :ok end - Enum.each(1..rounds, run) - :ok -end, max_concurrency: concurrency, ordered: false, timeout: :infinity) - -Enum.map(stream, fn result -> - result -end) -|> IO.inspect - -# defmodule Helper do -# def flush() do -# receive do -# msg -> -# IO.inspect(msg) -# flush() -# after -# 0 -> :ok -# end -# end -# end -# Helper.flush() - -IO.puts("Succeed!") +end + +for adapter <- [Gun, Mint] do + Logger.info("Starting run for adapter: #{adapter}") + args = [adapter, port, rounds] + stream_opts = [max_concurrency: concurrency, ordered: false, timeout: :infinity] + 1..concurrency + |> Task.async_stream(InteropTestRunner, :run, args, stream_opts) + |> Enum.to_list() +end + +Logger.info("Succeed!") :ok = GRPC.Server.stop_endpoint(Interop.Endpoint) diff --git a/lib/google/api/annotations.pb.ex b/lib/google/api/annotations.pb.ex new file mode 100644 index 000000000..374877d35 --- /dev/null +++ b/lib/google/api/annotations.pb.ex @@ -0,0 +1,8 @@ +defmodule Google.Api.PbExtension do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + extend Google.Protobuf.MethodOptions, :http, 72_295_728, + optional: true, + type: Google.Api.HttpRule +end diff --git a/lib/google/api/http.pb.ex b/lib/google/api/http.pb.ex new file mode 100644 index 000000000..25dd83ad1 --- /dev/null +++ b/lib/google/api/http.pb.ex @@ -0,0 +1,43 @@ +defmodule Google.Api.Http do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :rules, 1, repeated: true, type: Google.Api.HttpRule + + field :fully_decode_reserved_expansion, 2, + type: :bool, + json_name: "fullyDecodeReservedExpansion" +end + +defmodule Google.Api.HttpRule do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + oneof :pattern, 0 + + field :selector, 1, type: :string + field :get, 2, type: :string, oneof: 0 + field :put, 3, type: :string, oneof: 0 + field :post, 4, type: :string, oneof: 0 + field :delete, 5, type: :string, oneof: 0 + field :patch, 6, type: :string, oneof: 0 + field :custom, 8, type: Google.Api.CustomHttpPattern, oneof: 0 + field :body, 7, type: :string + field :response_body, 12, type: :string, json_name: "responseBody" + + field :additional_bindings, 11, + repeated: true, + type: Google.Api.HttpRule, + json_name: "additionalBindings" +end + +defmodule Google.Api.CustomHttpPattern do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :kind, 1, type: :string + field :path, 2, type: :string +end diff --git a/lib/google/protobuf/any.pb.ex b/lib/google/protobuf/any.pb.ex deleted file mode 100644 index db9d2f1e7..000000000 --- a/lib/google/protobuf/any.pb.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Google.Protobuf.Any do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - type_url: String.t(), - value: binary - } - - defstruct [:type_url, :value] - - field :type_url, 1, type: :string - field :value, 2, type: :bytes -end diff --git a/lib/google/protobuf/duration.pb.ex b/lib/google/protobuf/duration.pb.ex deleted file mode 100644 index 0faf3894a..000000000 --- a/lib/google/protobuf/duration.pb.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Google.Protobuf.Duration do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - seconds: integer, - nanos: integer - } - - defstruct [:seconds, :nanos] - - field :seconds, 1, type: :int64 - field :nanos, 2, type: :int32 -end diff --git a/lib/google/protobuf/struct.pb.ex b/lib/google/protobuf/struct.pb.ex deleted file mode 100644 index f7ca5e46d..000000000 --- a/lib/google/protobuf/struct.pb.ex +++ /dev/null @@ -1,67 +0,0 @@ -defmodule Google.Protobuf.NullValue do - @moduledoc false - use Protobuf, enum: true, syntax: :proto3 - @type t :: integer | :NULL_VALUE - - field :NULL_VALUE, 0 -end - -defmodule Google.Protobuf.Struct.FieldsEntry do - @moduledoc false - use Protobuf, map: true, syntax: :proto3 - - @type t :: %__MODULE__{ - key: String.t(), - value: Google.Protobuf.Value.t() | nil - } - - defstruct [:key, :value] - - field :key, 1, type: :string - field :value, 2, type: Google.Protobuf.Value -end - -defmodule Google.Protobuf.Struct do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - fields: %{String.t() => Google.Protobuf.Value.t() | nil} - } - - defstruct [:fields] - - field :fields, 1, repeated: true, type: Google.Protobuf.Struct.FieldsEntry, map: true -end - -defmodule Google.Protobuf.Value do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - kind: {atom, any} - } - - defstruct [:kind] - - oneof :kind, 0 - field :null_value, 1, type: Google.Protobuf.NullValue, enum: true, oneof: 0 - field :number_value, 2, type: :double, oneof: 0 - field :string_value, 3, type: :string, oneof: 0 - field :bool_value, 4, type: :bool, oneof: 0 - field :struct_value, 5, type: Google.Protobuf.Struct, oneof: 0 - field :list_value, 6, type: Google.Protobuf.ListValue, oneof: 0 -end - -defmodule Google.Protobuf.ListValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - values: [Google.Protobuf.Value.t()] - } - - defstruct [:values] - - field :values, 1, repeated: true, type: Google.Protobuf.Value -end diff --git a/lib/google/protobuf/timestamp.pb.ex b/lib/google/protobuf/timestamp.pb.ex deleted file mode 100644 index cb58cbfa4..000000000 --- a/lib/google/protobuf/timestamp.pb.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Google.Protobuf.Timestamp do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - seconds: integer, - nanos: integer - } - - defstruct [:seconds, :nanos] - - field :seconds, 1, type: :int64 - field :nanos, 2, type: :int32 -end diff --git a/lib/google/protobuf/wrappers.pb.ex b/lib/google/protobuf/wrappers.pb.ex deleted file mode 100644 index 3cc83d079..000000000 --- a/lib/google/protobuf/wrappers.pb.ex +++ /dev/null @@ -1,116 +0,0 @@ -defmodule Google.Protobuf.DoubleValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: float | :infinity | :negative_infinity | :nan - } - - defstruct [:value] - - field :value, 1, type: :double -end - -defmodule Google.Protobuf.FloatValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: float | :infinity | :negative_infinity | :nan - } - - defstruct [:value] - - field :value, 1, type: :float -end - -defmodule Google.Protobuf.Int64Value do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: integer - } - - defstruct [:value] - - field :value, 1, type: :int64 -end - -defmodule Google.Protobuf.UInt64Value do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: non_neg_integer - } - - defstruct [:value] - - field :value, 1, type: :uint64 -end - -defmodule Google.Protobuf.Int32Value do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: integer - } - - defstruct [:value] - - field :value, 1, type: :int32 -end - -defmodule Google.Protobuf.UInt32Value do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: non_neg_integer - } - - defstruct [:value] - - field :value, 1, type: :uint32 -end - -defmodule Google.Protobuf.BoolValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: boolean - } - - defstruct [:value] - - field :value, 1, type: :bool -end - -defmodule Google.Protobuf.StringValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: String.t() - } - - defstruct [:value] - - field :value, 1, type: :string -end - -defmodule Google.Protobuf.BytesValue do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - value: binary - } - - defstruct [:value] - - field :value, 1, type: :bytes -end diff --git a/lib/google/rpc/error_details.pb.ex b/lib/google/rpc/error_details.pb.ex index 9eb61d59e..c4ac40fe6 100644 --- a/lib/google/rpc/error_details.pb.ex +++ b/lib/google/rpc/error_details.pb.ex @@ -1,26 +1,13 @@ defmodule Google.Rpc.RetryInfo do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - retry_delay: Google.Protobuf.Duration.t() | nil - } - - defstruct [:retry_delay] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :retry_delay, 1, type: Google.Protobuf.Duration end defmodule Google.Rpc.DebugInfo do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - stack_entries: [String.t()], - detail: String.t() - } - - defstruct [:stack_entries, :detail] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :stack_entries, 1, repeated: true, type: :string field :detail, 2, type: :string @@ -28,14 +15,7 @@ end defmodule Google.Rpc.QuotaFailure.Violation do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - subject: String.t(), - description: String.t() - } - - defstruct [:subject, :description] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :subject, 1, type: :string field :description, 2, type: :string @@ -43,27 +23,14 @@ end defmodule Google.Rpc.QuotaFailure do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - violations: [Google.Rpc.QuotaFailure.Violation.t()] - } - - defstruct [:violations] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :violations, 1, repeated: true, type: Google.Rpc.QuotaFailure.Violation end defmodule Google.Rpc.ErrorInfo.MetadataEntry do @moduledoc false - use Protobuf, map: true, syntax: :proto3 - - @type t :: %__MODULE__{ - key: String.t(), - value: String.t() - } - - defstruct [:key, :value] + use Protobuf, map: true, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :key, 1, type: :string field :value, 2, type: :string @@ -71,15 +38,7 @@ end defmodule Google.Rpc.ErrorInfo do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - reason: String.t(), - domain: String.t(), - metadata: %{String.t() => String.t()} - } - - defstruct [:reason, :domain, :metadata] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :reason, 1, type: :string field :domain, 2, type: :string @@ -88,15 +47,7 @@ end defmodule Google.Rpc.PreconditionFailure.Violation do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - type: String.t(), - subject: String.t(), - description: String.t() - } - - defstruct [:type, :subject, :description] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :type, 1, type: :string field :subject, 2, type: :string @@ -105,27 +56,14 @@ end defmodule Google.Rpc.PreconditionFailure do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - violations: [Google.Rpc.PreconditionFailure.Violation.t()] - } - - defstruct [:violations] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :violations, 1, repeated: true, type: Google.Rpc.PreconditionFailure.Violation end defmodule Google.Rpc.BadRequest.FieldViolation do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - field: String.t(), - description: String.t() - } - - defstruct [:field, :description] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :field, 1, type: :string field :description, 2, type: :string @@ -133,27 +71,14 @@ end defmodule Google.Rpc.BadRequest do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - field_violations: [Google.Rpc.BadRequest.FieldViolation.t()] - } - - defstruct [:field_violations] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :field_violations, 1, repeated: true, type: Google.Rpc.BadRequest.FieldViolation end defmodule Google.Rpc.RequestInfo do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - request_id: String.t(), - serving_data: String.t() - } - - defstruct [:request_id, :serving_data] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :request_id, 1, type: :string field :serving_data, 2, type: :string @@ -161,16 +86,7 @@ end defmodule Google.Rpc.ResourceInfo do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - resource_type: String.t(), - resource_name: String.t(), - owner: String.t(), - description: String.t() - } - - defstruct [:resource_type, :resource_name, :owner, :description] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :resource_type, 1, type: :string field :resource_name, 2, type: :string @@ -180,14 +96,7 @@ end defmodule Google.Rpc.Help.Link do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - description: String.t(), - url: String.t() - } - - defstruct [:description, :url] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :description, 1, type: :string field :url, 2, type: :string @@ -195,27 +104,14 @@ end defmodule Google.Rpc.Help do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - links: [Google.Rpc.Help.Link.t()] - } - - defstruct [:links] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :links, 1, repeated: true, type: Google.Rpc.Help.Link end defmodule Google.Rpc.LocalizedMessage do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - locale: String.t(), - message: String.t() - } - - defstruct [:locale, :message] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :locale, 1, type: :string field :message, 2, type: :string diff --git a/lib/google/rpc/status.pb.ex b/lib/google/rpc/status.pb.ex index a254a1306..78cffd4da 100644 --- a/lib/google/rpc/status.pb.ex +++ b/lib/google/rpc/status.pb.ex @@ -1,14 +1,6 @@ defmodule Google.Rpc.Status do @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - code: integer, - message: String.t(), - details: [Google.Protobuf.Any.t()] - } - - defstruct [:code, :message, :details] + use Protobuf, syntax: :proto3, protoc_gen_elixir_version: "0.14.0" field :code, 1, type: :int32 field :message, 2, type: :string diff --git a/lib/grpc/adapter/cowboy/handler.ex b/lib/grpc/adapter/cowboy/handler.ex deleted file mode 100644 index b0ee22dff..000000000 --- a/lib/grpc/adapter/cowboy/handler.ex +++ /dev/null @@ -1,508 +0,0 @@ -defmodule GRPC.Adapter.Cowboy.Handler do - @moduledoc false - - # A cowboy handler accepting all requests and calls corresponding functions - # defined by users. - - alias GRPC.Transport.HTTP2 - alias GRPC.RPCError - require Logger - - @adapter GRPC.Adapter.Cowboy - @default_trailers HTTP2.server_trailers() - @type state :: %{ - pid: pid, - handling_timer: reference | nil, - resp_trailers: map, - compressor: atom | nil, - pending_reader: nil - } - - @spec init(map, {atom, GRPC.Server.servers_map(), map}) :: {:cowboy_loop, map, map} - def init(req, {endpoint, servers, opts} = state) do - path = :cowboy_req.path(req) - - with {:ok, server} <- find_server(servers, path), - {:ok, codec} <- find_codec(req, server), - # can be nil - {:ok, compressor} <- find_compressor(req, server) do - stream = %GRPC.Server.Stream{ - server: server, - endpoint: endpoint, - adapter: @adapter, - payload: %{pid: self()}, - local: opts[:local], - codec: codec, - compressor: compressor - } - - pid = spawn_link(__MODULE__, :call_rpc, [server, path, stream]) - Process.flag(:trap_exit, true) - - req = :cowboy_req.set_resp_headers(HTTP2.server_headers(stream), req) - - timeout = :cowboy_req.header("grpc-timeout", req) - - timer_ref = - if is_binary(timeout) do - Process.send_after( - self(), - {:handling_timeout, self()}, - GRPC.Transport.Utils.decode_timeout(timeout) - ) - end - - {:cowboy_loop, req, %{pid: pid, handling_timer: timer_ref, pending_reader: nil}} - else - {:error, error} -> - trailers = HTTP2.server_trailers(error.status, error.message) - req = send_error_trailers(req, trailers) - {:ok, req, state} - end - end - - defp find_server(servers, path) do - case Map.fetch(servers, GRPC.Server.service_name(path)) do - s = {:ok, _} -> - s - - _ -> - {:error, RPCError.exception(status: :unimplemented)} - end - end - - defp find_codec(req, server) do - req_content_type = :cowboy_req.header("content-type", req) - - {:ok, subtype} = extract_subtype(req_content_type) - codec = Enum.find(server.__meta__(:codecs), nil, fn c -> c.name() == subtype end) - - if codec do - {:ok, codec} - else - # TODO: Send grpc-accept-encoding header - {:error, - RPCError.exception( - status: :unimplemented, - message: "No codec registered for content-type #{req_content_type}" - )} - end - end - - defp find_compressor(req, server) do - encoding = :cowboy_req.header("grpc-encoding", req) - - if is_binary(encoding) do - compressor = Enum.find(server.__meta__(:compressors), nil, fn c -> c.name() == encoding end) - - if compressor do - {:ok, compressor} - else - {:error, - RPCError.exception( - status: :unimplemented, - message: "Not found compressor registered for grpc-encoding #{encoding}" - )} - end - else - {:ok, nil} - end - end - - # APIs begin - def read_full_body(pid) do - sync_call(pid, :read_full_body) - end - - def read_body(pid) do - sync_call(pid, :read_body) - end - - def stream_body(pid, data, opts, is_fin) do - send(pid, {:stream_body, data, opts, is_fin}) - end - - def stream_reply(pid, status, headers) do - send(pid, {:stream_reply, status, headers}) - end - - def set_resp_headers(pid, headers) do - send(pid, {:set_resp_headers, headers}) - end - - def set_resp_trailers(pid, trailers) do - send(pid, {:set_resp_trailers, trailers}) - end - - def set_compressor(pid, compressor) do - send(pid, {:set_compressor, compressor}) - end - - def stream_trailers(pid, trailers) do - send(pid, {:stream_trailers, trailers}) - end - - def get_headers(pid) do - sync_call(pid, :get_headers) - end - - def get_peer(pid) do - sync_call(pid, :get_peer) - end - - def get_cert(pid) do - sync_call(pid, :get_cert) - end - - defp sync_call(pid, key) do - ref = make_ref() - send(pid, {key, ref, self()}) - - receive do - {^ref, msg} -> msg - end - end - - # APIs end - - def info({:read_full_body, ref, pid}, req, state) do - {s, body, req} = read_full_body(req, "", state[:handling_timer]) - send(pid, {ref, {s, body}}) - {:ok, req, state} - catch - :exit, :timeout -> - Logger.warn("Timeout when reading full body") - info({:handling_timeout, self()}, req, state) - end - - def info({:read_body, ref, pid}, req, state) do - opts = timeout_left_opt(state[:handling_timer]) - - case async_read_body(req, opts) do - {:send, {s, body, req}} -> - send(pid, {ref, {s, body}}) - {:ok, req, state} - - {:wait, read_ref} -> - {:ok, req, %{state | pending_reader: {read_ref, pid, ref}}} - end - end - - def info({:request_body, ref, :nofin, body}, req, %{pending_reader: {ref, pid, reader_ref}} = s) do - send(pid, {reader_ref, {:more, body}}) - {:ok, req, %{s | pending_reader: nil}} - end - - def info( - {:request_body, ref, :fin, body_length, body}, - %{headers: headers} = req, - %{pending_reader: {ref, pid, reader_ref}} = s - ) do - send(pid, {reader_ref, {:ok, body}}) - - # cowboy_req's set_body_length - req = - Map.merge(req, %{ - headers: Map.put(headers, "content-length", Integer.to_string(body_length)), - body_length: body_length, - has_read_body: true - }) - - {:ok, req, s} - end - - def info({:get_headers, ref, pid}, req, state) do - headers = :cowboy_req.headers(req) - send(pid, {ref, headers}) - {:ok, req, state} - end - - def info({:get_peer, ref, pid}, req, state) do - peer = :cowboy_req.peer(req) - send(pid, {ref, peer}) - {:ok, req, state} - end - - def info({:get_cert, ref, pid}, req, state) do - peer = :cowboy_req.cert(req) - send(pid, {ref, peer}) - {:ok, req, state} - end - - def info({:stream_body, data, opts, is_fin}, req, state) do - # If compressor exists, compress is true by default - compressor = - if opts[:compress] == false do - nil - else - state[:compressor] - end - - accepted_encodings = - case :cowboy_req.header("grpc-accept-encoding", req) do - s when is_binary(s) -> - String.split(s, ",") - - _ -> - [] - end - - if compressor && !Enum.member?(accepted_encodings, compressor.name()) do - msg = - "A unaccepted encoding #{compressor.name()} is set, valid are: #{ - :cowboy_req.header("grpc-accept-encoding", req) - }" - - req = send_error(req, state, msg) - {:stop, req, state} - else - case GRPC.Message.to_data(data, compressor: compressor) do - {:ok, data, _size} -> - req = check_sent_resp(req) - :cowboy_req.stream_body(data, is_fin, req) - {:ok, req, state} - - {:error, msg} -> - req = send_error(req, state, msg) - {:stop, req, state} - end - end - end - - def info({:stream_reply, status, headers}, req, state) do - req = :cowboy_req.stream_reply(status, headers, req) - {:ok, req, state} - end - - def info({:set_resp_headers, headers}, req, state) do - req = :cowboy_req.set_resp_headers(headers, req) - {:ok, req, state} - end - - def info({:set_resp_trailers, trailers}, req, state) do - {:ok, req, Map.put(state, :resp_trailers, trailers)} - end - - def info({:stream_trailers, trailers}, req, state) do - metadata = Map.get(state, :resp_trailers, %{}) - metadata = GRPC.Transport.HTTP2.encode_metadata(metadata) - send_stream_trailers(req, Map.merge(metadata, trailers)) - {:ok, req, state} - end - - def info({:handling_timeout, _}, req, state = %{pid: pid}) do - error = %RPCError{status: GRPC.Status.deadline_exceeded(), message: "Deadline expired"} - trailers = HTTP2.server_trailers(error.status, error.message) - exit_handler(pid, :timeout) - req = send_error_trailers(req, trailers) - {:stop, req, state} - end - - def info({:set_compressor, compressor}, req, state) do - accept_encoding = :cowboy_req.header("grpc-accept-encoding", req) - - if is_binary(accept_encoding) do - req = :cowboy_req.set_resp_headers(%{"grpc-encoding" => compressor.name()}, req) - {:ok, req, Map.put(state, :compressor, compressor)} - else - {:ok, req, state} - end - end - - def info({:EXIT, pid, :normal}, req, state = %{pid: pid}) do - exit_handler(pid, :normal) - {:stop, req, state} - end - - # expected error raised from user to return error immediately - def info( - {:EXIT, pid, {%RPCError{details: details} = error, _stacktrace}}, - req, - state = %{pid: pid} - ) do - proto_status = GRPC.Transport.Utils.encode_status_details(error.status, details) - trailers = HTTP2.server_trailers(error.status, error.message, proto_status) - exit_handler(pid, :rpc_error) - req = send_error_trailers(req, trailers) - {:stop, req, state} - end - - # unknown error raised from rpc - def info({:EXIT, pid, {:handle_error, _kind}}, req, state = %{pid: pid}) do - error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"} - trailers = HTTP2.server_trailers(error.status, error.message) - exit_handler(pid, :error) - req = send_error_trailers(req, trailers) - {:stop, req, state} - end - - def info({:EXIT, pid, {reason, stacktrace}}, req, state = %{pid: pid}) do - Logger.error(Exception.format(:error, reason, stacktrace)) - error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"} - trailers = HTTP2.server_trailers(error.status, error.message) - exit_handler(pid, reason) - req = send_error_trailers(req, trailers) - {:stop, req, state} - end - - def terminate(reason, _req, %{pid: pid}) do - exit_handler(pid, reason) - :ok - end - - def terminate(_reason, _req, _state) do - :ok - end - - def call_rpc(server, path, stream) do - result = - try do - case do_call_rpc(server, path, stream) do - {:error, _} = err -> - err - - _ -> - :ok - end - catch - kind, reason -> - reason = Exception.normalize(kind, reason, __STACKTRACE__) - - crash_reason = - case kind do - :throw -> {{:nocatch, reason}, __STACKTRACE__} - _ -> {reason, __STACKTRACE__} - end - - Logger.error( - Exception.format(kind, reason, __STACKTRACE__), - crash_reason: crash_reason - ) - - exit({:handle_error, kind}) - end - - case result do - {:error, %GRPC.RPCError{} = e} -> - exit({e, ""}) - - {:error, %{kind: kind}} -> - exit({:handle_error, kind}) - - other -> - other - end - end - - defp do_call_rpc(server, path, stream) do - result = server.__call_rpc__(path, stream) - - case result do - {:ok, stream, response} -> - stream - |> GRPC.Server.send_reply(response) - |> GRPC.Server.send_trailers(@default_trailers) - - {:ok, stream} - - {:ok, stream} -> - GRPC.Server.send_trailers(stream, @default_trailers) - {:ok, stream} - - error -> - error - end - end - - defp read_full_body(req, body, timer) do - result = :cowboy_req.read_body(req, timeout_left_opt(timer)) - - case result do - {:ok, data, req} -> {:ok, body <> data, req} - {:more, data, req} -> read_full_body(req, body <> data, timer) - end - end - - defp send_stream_trailers(req, trailers) do - req = check_sent_resp(req) - :cowboy_req.stream_trailers(trailers, req) - end - - defp check_sent_resp(%{has_sent_resp: _} = req) do - req - end - - defp check_sent_resp(req) do - :cowboy_req.stream_reply(200, req) - end - - defp send_error_trailers(%{has_sent_resp: _} = req, trailers) do - :cowboy_req.stream_trailers(trailers, req) - end - - defp send_error_trailers(req, trailers) do - :cowboy_req.reply(200, trailers, req) - end - - def exit_handler(pid, reason) do - if Process.alive?(pid) do - Process.exit(pid, reason) - end - end - - defp timeout_left_opt(timer, opts \\ %{}) do - case timer do - nil -> - Map.put(opts, :timeout, :infinity) - - timer -> - case Process.read_timer(timer) do - ms when is_integer(ms) -> - Map.put(opts, :timeout, ms) - - _ -> - Map.put(opts, :timeout, 0) - end - end - end - - defp extract_subtype("application/grpc"), do: {:ok, "proto"} - defp extract_subtype("application/grpc+"), do: {:ok, "proto"} - defp extract_subtype("application/grpc;"), do: {:ok, "proto"} - - defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, rest} - defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, rest} - - defp extract_subtype(type) do - Logger.warn("Got unknown content-type #{type}, please create an issue.") - {:ok, "proto"} - end - - defp send_error(req, %{pid: pid}, msg) do - error = RPCError.exception(status: :internal, message: msg) - trailers = HTTP2.server_trailers(error.status, error.message) - - exit_handler(pid, :rpc_error) - send_error_trailers(req, trailers) - end - - # Similar with cowboy's read_body, but we need to receive the message - # in `info` callback. - defp async_read_body(%{has_body: false} = req, _opts) do - {:send, {:ok, <<>>, req}} - end - - defp async_read_body(%{has_read_body: true} = req, _opts) do - {:send, {:ok, <<>>, req}} - end - - defp async_read_body(req, opts) do - length = Map.get(opts, :length, 8_000_000) - period = Map.get(opts, :period, 15000) - ref = make_ref() - - :cowboy_req.cast({:read_body, self(), ref, length, period}, req) - - {:wait, ref} - end -end diff --git a/lib/grpc/adapter/gun.ex b/lib/grpc/adapter/gun.ex deleted file mode 100644 index cc350a026..000000000 --- a/lib/grpc/adapter/gun.ex +++ /dev/null @@ -1,319 +0,0 @@ -defmodule GRPC.Adapter.Gun do - @moduledoc false - - # A client adapter using Gun. - # conn_pid and stream_ref is stored in `GRPC.Server.Stream`. - - @default_transport_opts [nodelay: true] - @default_http2_opts %{settings_timeout: :infinity} - @max_retries 100 - - @spec connect(GRPC.Channel.t(), any) :: {:ok, GRPC.Channel.t()} | {:error, any} - def connect(channel, nil), do: connect(channel, %{}) - def connect(%{scheme: "https"} = channel, opts), do: connect_securely(channel, opts) - def connect(channel, opts), do: connect_insecurely(channel, opts) - - defp connect_securely(%{cred: %{ssl: ssl}} = channel, opts) do - transport_opts = Map.get(opts, :transport_opts, @default_transport_opts ++ ssl) - open_opts = %{transport: :ssl, protocols: [:http2]} - - open_opts = - if gun_v2?() do - Map.put(open_opts, :tls_opts, transport_opts) - else - Map.put(open_opts, :transport_opts, transport_opts) - end - - open_opts = Map.merge(opts, open_opts) - - do_connect(channel, open_opts) - end - - defp connect_insecurely(channel, opts) do - opts = Map.update(opts, :http2_opts, @default_http2_opts, &Map.merge(&1, @default_http2_opts)) - - transport_opts = Map.get(opts, :transport_opts, @default_transport_opts) - open_opts = %{transport: :tcp, protocols: [:http2]} - - open_opts = - if gun_v2?() do - Map.put(open_opts, :tcp_opts, transport_opts) - else - Map.put(open_opts, :transport_opts, transport_opts) - end - - open_opts = Map.merge(opts, open_opts) - - do_connect(channel, open_opts) - end - - defp do_connect(%{host: host, port: port} = channel, open_opts) do - open_opts = - if gun_v2?() do - Map.merge(%{retry: @max_retries, retry_fun: &__MODULE__.retry_fun/2}, open_opts) - else - open_opts - end - - {:ok, conn_pid} = open(host, port, open_opts) - - case :gun.await_up(conn_pid) do - {:ok, :http2} -> - {:ok, Map.put(channel, :adapter_payload, %{conn_pid: conn_pid})} - - {:ok, proto} -> - :gun.shutdown(conn_pid) - {:error, "Error when opening connection: protocol #{proto} is not http2"} - - {:error, reason} -> - :gun.shutdown(conn_pid) - {:error, "Error when opening connection: #{inspect(reason)}"} - end - end - - def disconnect(%{adapter_payload: %{conn_pid: gun_pid}} = channel) - when is_pid(gun_pid) do - :ok = :gun.shutdown(gun_pid) - {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} - end - - def disconnect(%{adapter_payload: %{conn_pid: nil}} = channel) do - {:ok, channel} - end - - defp open({:local, socket_path}, _port, open_opts), - do: :gun.open_unix(socket_path, open_opts) - - defp open(host, port, open_opts), - do: :gun.open(String.to_charlist(host), port, open_opts) - - @spec send_request(GRPC.Client.Stream.t(), binary, map) :: GRPC.Client.Stream.t() - def send_request(stream, message, opts) do - stream_ref = do_send_request(stream, message, opts) - GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) - end - - defp do_send_request( - %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, path: path} = stream, - message, - opts - ) do - headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) - {:ok, data, _} = GRPC.Message.to_data(message, opts) - :gun.post(conn_pid, path, headers, data) - end - - def send_headers( - %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, path: path} = stream, - opts - ) do - headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) - stream_ref = :gun.post(conn_pid, path, headers) - GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) - end - - def send_data(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream, message, opts) do - conn_pid = channel.adapter_payload[:conn_pid] - fin = if opts[:send_end_stream], do: :fin, else: :nofin - {:ok, data, _} = GRPC.Message.to_data(message, opts) - :gun.data(conn_pid, stream_ref, fin, data) - stream - end - - def end_stream(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream) do - conn_pid = channel.adapter_payload[:conn_pid] - :gun.data(conn_pid, stream_ref, :fin, "") - stream - end - - def cancel(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}) do - :gun.cancel(conn_pid, stream_ref) - end - - def recv_headers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do - case await(conn_pid, stream_ref, opts[:timeout]) do - {:response, headers, fin} -> - {:ok, headers, fin} - - error = {:error, _, _headers} -> - error - - other -> - {:error, - GRPC.RPCError.exception( - GRPC.Status.unknown(), - "unexpected when waiting for headers: #{inspect(other)}" - )} - end - end - - def recv_data_or_trailers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do - case await(conn_pid, stream_ref, opts[:timeout]) do - data = {:data, _} -> - data - - trailers = {:trailers, _} -> - trailers - - error = {:error, _, _} -> - error - - other -> - {:error, - GRPC.RPCError.exception( - GRPC.Status.unknown(), - "unexpected when waiting for data: #{inspect(other)}" - ), %{}} - end - end - - @spec await(pid, reference, timeout) :: - {:response, map, :fin | :nofin} - | {:data, iodata} - | {:trailers, map} - | {:error, map, map} - defp await(conn_pid, stream_ref, timeout) do - # We should use server timeout for most time - timeout = - if is_integer(timeout) do - timeout * 2 - else - timeout - end - - result = :gun.await(conn_pid, stream_ref, timeout) - - case result do - {:response, :fin, status, headers} -> - if status == 200 do - headers = GRPC.Transport.HTTP2.decode_headers(headers) - - case headers["grpc-status"] do - nil -> - {:error, - GRPC.RPCError.exception( - GRPC.Status.internal(), - "shouldn't finish when getting headers" - ), headers} - - "0" -> - {:response, headers, :fin} - - _ -> - {:error, - GRPC.RPCError.exception( - String.to_integer(headers["grpc-status"]), - headers["grpc-message"], - decode_status_details(headers) - ), headers} - end - else - headers = Enum.into(headers, %{}) - - {:error, - GRPC.RPCError.exception( - GRPC.Status.internal(), - "status got is #{status} instead of 200" - ), headers} - end - - {:response, :nofin, status, headers} -> - if status == 200 do - headers = GRPC.Transport.HTTP2.decode_headers(headers) - - if headers["grpc-status"] && headers["grpc-status"] != "0" do - {:error, - GRPC.RPCError.exception( - String.to_integer(headers["grpc-status"]), - headers["grpc-message"], - decode_status_details(headers) - ), headers} - else - {:response, headers, :nofin} - end - else - headers = Enum.into(headers, %{}) - - {:error, - GRPC.RPCError.exception( - GRPC.Status.internal(), - "status got is #{status} instead of 200" - ), headers} - end - - {:data, :fin, data} -> - {:data, data} - - {:data, :nofin, data} -> - {:data, data} - - trailers = {:trailers, _} -> - trailers - - {:error, :timeout} -> - {:error, - GRPC.RPCError.exception( - GRPC.Status.deadline_exceeded(), - "timeout when waiting for server" - ), %{}} - - {:error, {reason, msg}} when reason in [:stream_error, :connection_error] -> - error = - GRPC.RPCError.exception(GRPC.Status.internal(), "#{inspect(reason)}: #{inspect(msg)}") - - {:error, error, %{}} - - {:error, {reason, msg}} -> - error = - GRPC.RPCError.exception(GRPC.Status.unknown(), "#{inspect(reason)}: #{inspect(msg)}") - - {:error, error, %{}} - - other -> - {:error, - GRPC.RPCError.exception( - GRPC.Status.unknown(), - "unexpected message when waiting for server: #{inspect(other)}" - ), %{}} - end - end - - @char_2 List.first('2') - def gun_v2?() do - case :application.get_key(:gun, :vsn) do - {:ok, [@char_2 | _]} -> - true - - _ -> - false - end - end - - def retry_fun(retries, _opts) do - curr = @max_retries - retries + 1 - - timeout = - if curr < 11 do - :math.pow(1.6, curr - 1) * 1000 - else - 120_000 - end - - jitter = - if function_exported?(:rand, :uniform_real, 0) do - (:rand.uniform_real() - 0.5) / 2.5 - else - (:rand.uniform() - 0.5) / 2.5 - end - - timeout = round(timeout + jitter * timeout) - %{retries: retries - 1, timeout: timeout} - end - - defp decode_status_details(%{"grpc-status-details-bin" => details}) - when is_binary(details) do - GRPC.Transport.Utils.decode_status_details(details) - end - - defp decode_status_details(_headers), do: nil -end diff --git a/lib/grpc/channel.ex b/lib/grpc/channel.ex index 09194cc11..d7818b07b 100644 --- a/lib/grpc/channel.ex +++ b/lib/grpc/channel.ex @@ -11,23 +11,23 @@ defmodule GRPC.Channel do * `:port` - server's port to connect * `:scheme` - scheme of connection, like `http` * `:cred` - credentials used for authentication - * `:adapter` - a client adapter module, like `GRPC.Adapter.Gun` + * `:adapter` - a client adapter module, like `GRPC.Client.Adapters.Gun` * `:codec` - a default codec for this channel * `:adapter_payload` - payload the adapter uses """ @type t :: %__MODULE__{ host: String.t(), - port: non_neg_integer, + port: non_neg_integer(), scheme: String.t(), cred: GRPC.Credential.t(), - adapter: atom, - adapter_payload: any, - codec: module, + adapter: atom(), + adapter_payload: any(), + codec: module(), interceptors: [], - compressor: module, - accepted_compressors: [module], - headers: list + compressor: module(), + accepted_compressors: [module()], + headers: list() } defstruct host: nil, port: nil, diff --git a/lib/grpc/client/adapter.ex b/lib/grpc/client/adapter.ex new file mode 100644 index 000000000..427bf5e52 --- /dev/null +++ b/lib/grpc/client/adapter.ex @@ -0,0 +1,51 @@ +defmodule GRPC.Client.Adapter do + @moduledoc """ + HTTP client adapter for GRPC. + """ + + alias GRPC.Client.Stream + alias GRPC.Channel + + @typedoc "Determines if the headers have finished being read." + @type fin :: :fin | :nofin + + @callback connect(channel :: Channel.t(), opts :: keyword()) :: + {:ok, Channel.t()} | {:error, any()} + + @callback disconnect(channel :: Channel.t()) :: {:ok, Channel.t()} | {:error, any()} + + @callback send_request(stream :: Stream.t(), contents :: iodata(), opts :: keyword()) :: + Stream.t() + + @doc """ + Check `GRPC.Stub.recv/2` for more context about the return types + """ + @callback receive_data(stream :: Stream.t(), opts :: keyword()) :: + GRPC.Stub.receive_data_return() | {:error, any()} + + @doc """ + This callback is used to open a stream connection to the server. + Mostly used when the payload for this request is streamed. + To send data using the open stream request, you should use `send_data/3` + """ + @callback send_headers(stream :: Stream.t(), opts :: keyword()) :: Stream.t() + + @doc """ + This callback will be responsible to send data to the server on a stream + request is open using `send_headers/2` + Opts: + - :send_end_stream (optional) - ends the request stream + """ + @callback send_data(stream :: Stream.t(), message :: iodata(), opts :: keyword()) :: Stream.t() + + @doc """ + Similarly to the option sent on `send_data/2` - :send_end_stream - + this callback will end request stream + """ + @callback end_stream(stream :: Stream.t()) :: Stream.t() + + @doc """ + Cancel a stream in a streaming client. + """ + @callback cancel(stream :: Stream.t()) :: :ok | {:error, any()} +end diff --git a/lib/grpc/client/adapters/gun.ex b/lib/grpc/client/adapters/gun.ex new file mode 100644 index 000000000..6f8fe7b7e --- /dev/null +++ b/lib/grpc/client/adapters/gun.ex @@ -0,0 +1,473 @@ +defmodule GRPC.Client.Adapters.Gun do + @moduledoc """ + A client adapter using Gun + + `conn_pid` and `stream_ref` are stored in `GRPC.Server.Stream`. + """ + + @behaviour GRPC.Client.Adapter + + @default_tcp_opts [nodelay: true] + @max_retries 100 + + @impl true + def connect(channel, opts) when is_list(opts) do + # handle opts as a map due to :gun.open + opts = Map.new(opts) + + case channel do + %{scheme: "https"} -> connect_securely(channel, opts) + _ -> connect_insecurely(channel, opts) + end + end + + defp connect_securely(%{cred: %{ssl: ssl}} = channel, opts) do + transport_opts = Map.get(opts, :transport_opts) || [] + + tls_opts = Keyword.merge(ssl, transport_opts) + + open_opts = + opts + |> Map.delete(:transport_opts) + |> Map.merge(%{ + transport: :ssl, + protocols: [:http2], + tcp_opts: @default_tcp_opts, + tls_opts: tls_opts + }) + + do_connect(channel, open_opts) + end + + defp connect_insecurely(channel, opts) do + opts = + Map.update( + opts, + :http2_opts, + %{settings_timeout: :infinity}, + &Map.put(&1, :settings_timeout, :infinity) + ) + + transport_opts = Map.get(opts, :transport_opts) || [] + + tcp_opts = Keyword.merge(@default_tcp_opts, transport_opts) + + open_opts = + opts + |> Map.delete(:transport_opts) + |> Map.merge(%{transport: :tcp, protocols: [:http2], tcp_opts: tcp_opts}) + + do_connect(channel, open_opts) + end + + defp do_connect(%{host: host, port: port} = channel, open_opts) do + open_opts = Map.merge(%{retry: @max_retries, retry_fun: &__MODULE__.retry_fun/2}, open_opts) + + {:ok, conn_pid} = open(host, port, open_opts) + + case :gun.await_up(conn_pid) do + {:ok, :http2} -> + {:ok, Map.put(channel, :adapter_payload, %{conn_pid: conn_pid})} + + {:ok, proto} -> + :gun.shutdown(conn_pid) + {:error, "Error when opening connection: protocol #{proto} is not http2"} + + {:error, reason} -> + :gun.shutdown(conn_pid) + {:error, reason} + end + end + + @impl true + def disconnect(%{adapter_payload: %{conn_pid: gun_pid}} = channel) + when is_pid(gun_pid) do + :ok = :gun.shutdown(gun_pid) + {:ok, %{channel | adapter_payload: %{conn_pid: nil}}} + end + + def disconnect(%{adapter_payload: %{conn_pid: nil}} = channel) do + {:ok, channel} + end + + defp open({:local, socket_path}, _port, open_opts), + do: :gun.open_unix(socket_path, open_opts) + + defp open(host, port, open_opts), + do: :gun.open(String.to_charlist(host), port, open_opts) + + @impl true + def send_request(stream, message, opts) do + stream_ref = do_send_request(stream, message, opts) + GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) + end + + defp do_send_request( + %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, path: path} = stream, + message, + opts + ) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + {:ok, data, _} = GRPC.Message.to_data(message, opts) + :gun.post(conn_pid, path, headers, data) + end + + @impl true + def send_headers( + %{channel: %{adapter_payload: %{conn_pid: conn_pid}}, path: path} = stream, + opts + ) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + stream_ref = :gun.post(conn_pid, path, headers) + GRPC.Client.Stream.put_payload(stream, :stream_ref, stream_ref) + end + + @impl true + def send_data(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream, message, opts) do + conn_pid = channel.adapter_payload[:conn_pid] + fin = if opts[:send_end_stream], do: :fin, else: :nofin + {:ok, data, _} = GRPC.Message.to_data(message, opts) + :gun.data(conn_pid, stream_ref, fin, data) + stream + end + + @impl true + def end_stream(%{channel: channel, payload: %{stream_ref: stream_ref}} = stream) do + conn_pid = channel.adapter_payload[:conn_pid] + :gun.data(conn_pid, stream_ref, :fin, "") + stream + end + + @impl true + def cancel(stream) do + %{ + channel: %{adapter_payload: %{conn_pid: conn_pid}}, + payload: %{stream_ref: stream_ref} + } = stream + + :gun.cancel(conn_pid, stream_ref) + end + + @impl true + def receive_data( + %{server_stream: true} = stream, + opts + ) do + %{channel: %{adapter_payload: adapter_payload}, payload: payload} = stream + + with {:ok, headers, is_fin} <- recv_headers(adapter_payload, payload, opts) do + response = response_stream(is_fin, stream, opts) + + if(opts[:return_headers]) do + {:ok, response, %{headers: headers}} + else + {:ok, response} + end + end + end + + def receive_data(stream, opts) do + %{payload: payload, channel: %{adapter_payload: adapter_payload}} = stream + + with {:ok, headers, _is_fin} <- recv_headers(adapter_payload, payload, opts), + {:ok, body, trailers} <- recv_body(adapter_payload, payload, opts), + {:ok, response} <- parse_response(stream, headers, body, trailers) do + if(opts[:return_headers]) do + {:ok, response, %{headers: headers, trailers: trailers}} + else + {:ok, response} + end + end + end + + defp recv_headers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do + case await(conn_pid, stream_ref, opts[:timeout]) do + {:response, headers, fin} -> + {:ok, headers, fin} + + {:error, _} = error -> + error + + other -> + {:error, + GRPC.RPCError.exception( + GRPC.Status.unknown(), + "unexpected when waiting for headers: #{inspect(other)}" + )} + end + end + + defp recv_data_or_trailers(%{conn_pid: conn_pid}, %{stream_ref: stream_ref}, opts) do + case await(conn_pid, stream_ref, opts[:timeout]) do + data = {:data, _} -> + data + + trailers = {:trailers, _} -> + trailers + + {:error, _} = error -> + error + + other -> + {:error, + GRPC.RPCError.exception( + GRPC.Status.unknown(), + "unexpected when waiting for data: #{inspect(other)}" + )} + end + end + + defp await(conn_pid, stream_ref, timeout) do + # We should use server timeout for most time + timeout = + if is_integer(timeout) do + timeout * 2 + else + timeout + end + + case :gun.await(conn_pid, stream_ref, timeout) do + {:response, :fin, status, headers} -> + if status == 200 do + headers = GRPC.Transport.HTTP2.decode_headers(headers) + + case headers["grpc-status"] do + nil -> + {:error, + GRPC.RPCError.exception( + GRPC.Status.internal(), + "shouldn't finish when getting headers" + )} + + "0" -> + {:response, headers, :fin} + + _ -> + {:error, + GRPC.RPCError.exception( + String.to_integer(headers["grpc-status"]), + headers["grpc-message"] + )} + end + else + {:error, + GRPC.RPCError.exception( + GRPC.Status.internal(), + "status got is #{status} instead of 200" + )} + end + + {:response, :nofin, status, headers} -> + if status == 200 do + headers = GRPC.Transport.HTTP2.decode_headers(headers) + + if headers["grpc-status"] && headers["grpc-status"] != "0" do + {:error, + GRPC.RPCError.exception( + String.to_integer(headers["grpc-status"]), + headers["grpc-message"] + )} + else + {:response, headers, :nofin} + end + else + {:error, + GRPC.RPCError.exception( + GRPC.Status.internal(), + "status got is #{status} instead of 200" + )} + end + + {:data, :fin, data} -> + {:data, data} + + {:data, :nofin, data} -> + {:data, data} + + trailers = {:trailers, _} -> + trailers + + {:error, :timeout} -> + {:error, + GRPC.RPCError.exception( + GRPC.Status.deadline_exceeded(), + "timeout when waiting for server" + )} + + {:error, {reason, msg}} when reason in [:stream_error, :connection_error] -> + {:error, + GRPC.RPCError.exception(GRPC.Status.internal(), "#{inspect(reason)}: #{inspect(msg)}")} + + {:error, {reason, msg}} -> + {:error, + GRPC.RPCError.exception(GRPC.Status.unknown(), "#{inspect(reason)}: #{inspect(msg)}")} + + other -> + {:error, + GRPC.RPCError.exception( + GRPC.Status.unknown(), + "unexpected message when waiting for server: #{inspect(other)}" + )} + end + end + + def retry_fun(retries, _opts) do + curr = @max_retries - retries + 1 + + timeout = + if curr < 11 do + :math.pow(1.6, curr - 1) * 1000 + else + 120_000 + end + + jitter = + if function_exported?(:rand, :uniform_real, 0) do + (:rand.uniform_real() - 0.5) / 2.5 + else + (:rand.uniform() - 0.5) / 2.5 + end + + timeout = round(timeout + jitter * timeout) + %{retries: retries - 1, timeout: timeout} + end + + defp recv_body(conn_payload, stream_payload, opts) do + recv_body(conn_payload, stream_payload, "", opts) + end + + defp recv_body(conn_payload, stream_payload, acc, opts) do + case recv_data_or_trailers(conn_payload, stream_payload, opts) do + {:data, data} -> + recv_body(conn_payload, stream_payload, <>, opts) + + {:trailers, trailers} -> + {:ok, acc, GRPC.Transport.HTTP2.decode_headers(trailers)} + + err -> + err + end + end + + defp response_stream(:fin, _stream, _opts), do: [] + + defp response_stream( + :nofin, + %{ + channel: %{adapter_payload: ap}, + response_mod: res_mod, + codec: codec, + payload: payload + }, + opts + ) do + state = %{ + adapter_payload: ap, + payload: payload, + buffer: <<>>, + fin: false, + need_more: true, + opts: opts, + response_mod: res_mod, + codec: codec + } + + Stream.unfold(state, fn s -> read_stream(s) end) + end + + defp read_stream(%{buffer: <<>>, fin: true, fin_resp: nil}), do: nil + + defp read_stream(%{buffer: <<>>, fin: true, fin_resp: fin_resp} = s), + do: {fin_resp, Map.put(s, :fin_resp, nil)} + + defp read_stream( + %{ + adapter_payload: ap, + payload: payload, + buffer: buffer, + need_more: true, + opts: opts + } = stream + ) do + case recv_data_or_trailers(ap, payload, opts) do + {:data, data} -> + stream + |> Map.put(:need_more, false) + |> Map.put(:buffer, buffer <> data) + |> read_stream() + + {:trailers, trailers} -> + update_stream_with_trailers(stream, trailers, opts[:return_headers]) + + error = {:error, _} -> + {error, %{buffer: <<>>, fin: true, fin_resp: nil}} + end + end + + defp read_stream(%{buffer: buffer, need_more: false, response_mod: res_mod, codec: codec} = s) do + case GRPC.Message.get_message(buffer) do + {{_, message}, rest} -> + reply = codec.decode(message, res_mod) + new_s = Map.put(s, :buffer, rest) + {{:ok, reply}, new_s} + + _ -> + read_stream(Map.put(s, :need_more, true)) + end + end + + defp parse_response( + %{response_mod: res_mod, codec: codec, accepted_compressors: accepted_compressors}, + headers, + body, + trailers + ) do + with :ok <- parse_trailers(trailers), + compressor <- get_compressor(headers, accepted_compressors), + body <- get_body(codec, body), + {:ok, msg} <- GRPC.Message.from_data(%{compressor: compressor}, body) do + {:ok, codec.decode(msg, res_mod)} + end + end + + defp update_stream_with_trailers(stream, trailers, return_headers?) do + trailers = GRPC.Transport.HTTP2.decode_headers(trailers) + + case parse_trailers(trailers) do + :ok -> + fin_resp = if return_headers?, do: {:trailers, trailers} + + stream + |> Map.put(:fin, true) + |> Map.put(:fin_resp, fin_resp) + |> read_stream() + + error -> + {error, %{buffer: <<>>, fin: true, fin_resp: nil}} + end + end + + defp parse_trailers(trailers) do + status = String.to_integer(trailers["grpc-status"]) + + if status == GRPC.Status.ok() do + :ok + else + {:error, %GRPC.RPCError{status: status, message: trailers["grpc-message"]}} + end + end + + defp get_compressor(%{"grpc-encoding" => encoding} = _headers, accepted_compressors) do + Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding end) + end + + defp get_compressor(_headers, _accepted_compressors), do: nil + + defp get_body(codec, body) do + if function_exported?(codec, :unpack_from_channel, 1) do + codec.unpack_from_channel(body) + else + body + end + end +end diff --git a/lib/grpc/client/adapters/mint.ex b/lib/grpc/client/adapters/mint.ex new file mode 100644 index 000000000..d82ad2d16 --- /dev/null +++ b/lib/grpc/client/adapters/mint.ex @@ -0,0 +1,248 @@ +defmodule GRPC.Client.Adapters.Mint do + @moduledoc """ + A client adapter using Mint. + """ + + alias GRPC.Channel + alias GRPC.Client.Adapters.Mint.ConnectionProcess + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + alias GRPC.Credential + + @behaviour GRPC.Client.Adapter + + @default_connect_opts [ + protocols: [:http2] + ] + @default_client_settings [ + initial_window_size: 8_000_000, + max_frame_size: 8_000_000 + ] + @default_transport_opts [timeout: :infinity] + + @doc """ + Connects using Mint based on the provided configs. Options + * `:transport_opts`: Defaults to `[timeout: :infinity]`, given the nature of H2 connections (with support to + long-lived streams) this default is set to avoid timeouts while waiting for server streams to complete. The other + options may vary based on the transport used for this connection (tcp or ssl). Check [Mint.HTTP.connect/4](https://hexdocs.pm/mint/Mint.HTTP.html#connect/4) + * `:client_settings`: Defaults to `[initial_window_size: 8_000_000, max_frame_size: 8_000_000]`, a larger default + window size ensures that the number of packages exchanges is smaller, thus speeding up the requests by reducing the + amount of networks round trip, with the cost of having larger packages reaching the server per connection. + Check [Mint.HTTP2.setting() type](https://hexdocs.pm/mint/Mint.HTTP2.html#t:setting/0) for additional configs. + """ + @impl true + def connect(%{host: host, port: port} = channel, opts \\ []) do + # Added :config_options to facilitate testing. + {config_opts, opts} = Keyword.pop(opts, :config_options, []) + module_opts = Application.get_env(:grpc, __MODULE__, config_opts) + + opts = connect_opts(channel, opts) |> merge_opts(module_opts) + + Process.flag(:trap_exit, true) + + channel + |> mint_scheme() + |> ConnectionProcess.start_link(host, port, opts) + |> case do + {:ok, pid} -> + {:ok, %{channel | adapter_payload: %{conn_pid: pid}}} + + error -> + {:error, "Error while opening connection: #{inspect(error)}"} + end + catch + :exit, reason -> + {:error, "Error while opening connection: #{inspect(reason)}"} + end + + @impl true + def disconnect(%{adapter_payload: %{conn_pid: pid}} = channel) + when is_pid(pid) do + :ok = ConnectionProcess.disconnect(pid) + {:ok, %{channel | adapter_payload: nil}} + end + + def disconnect(%{adapter_payload: nil} = channel) do + {:ok, channel} + end + + @impl true + def send_request(%{channel: %{adapter_payload: nil}}, _message, _opts), + do: raise(ArgumentError, "Can't perform a request without a connection process") + + def send_request(stream, message, opts) do + {:ok, data, _} = GRPC.Message.to_data(message, opts) + do_request(stream, opts, data) + end + + @impl true + def receive_data(stream, opts) do + if success_response?(stream) do + do_receive_data(stream, stream.grpc_type, opts) + else + handle_errors_receive_data(stream, opts) + end + end + + @impl true + def send_headers(%{channel: %{adapter_payload: nil}}, _opts), + do: raise("Can't start a client stream without a connection process") + + def send_headers(stream, opts) do + do_request(stream, opts, :stream) + end + + @impl true + def send_data( + %{ + channel: %{adapter_payload: %{conn_pid: pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream, + message, + opts + ) do + {:ok, data, _} = GRPC.Message.to_data(message, opts) + :ok = ConnectionProcess.stream_request_body(pid, request_ref, data) + if opts[:send_end_stream], do: ConnectionProcess.stream_request_body(pid, request_ref, :eof) + stream + end + + @impl true + def end_stream( + %{ + channel: %{adapter_payload: %{conn_pid: pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream + ) do + ConnectionProcess.stream_request_body(pid, request_ref, :eof) + stream + end + + @impl true + def cancel(stream) do + %{ + channel: %{adapter_payload: %{conn_pid: conn_pid}}, + payload: %{response: {:ok, %{request_ref: request_ref}}} + } = stream + + ConnectionProcess.cancel(conn_pid, request_ref) + end + + defp connect_opts(%Channel{scheme: "https"} = channel, opts) do + %Credential{ssl: ssl} = Map.get(channel, :cred) || %Credential{} + + transport_opts = + opts + |> Keyword.get(:transport_opts, []) + |> Keyword.merge(ssl) + + client_settings = Keyword.get(opts, :client_settings, @default_client_settings) + + [ + transport_opts: Keyword.merge(@default_transport_opts, transport_opts), + client_settings: client_settings + ] + end + + defp connect_opts(_channel, opts) do + transport_opts = Keyword.get(opts, :transport_opts, []) + client_settings = Keyword.get(opts, :client_settings, @default_client_settings) + + [ + transport_opts: Keyword.merge(@default_transport_opts, transport_opts), + client_settings: client_settings + ] + end + + defp merge_opts(opts, module_opts) do + opts = Keyword.merge(opts, module_opts) + Keyword.merge(@default_connect_opts, opts) + end + + defp mint_scheme(%Channel{scheme: "https"} = _channel), do: :https + defp mint_scheme(_channel), do: :http + + defp do_receive_data(%{payload: %{stream_response_pid: pid}}, request_type, opts) + when request_type in [:bidirectional_stream, :server_stream] do + produce_trailers? = opts[:return_headers] == true + stream = StreamResponseProcess.build_stream(pid, produce_trailers?) + headers_or_error = Enum.at(stream, 0) + # if this check fails then an error tuple will be returned + with {:headers, headers} <- headers_or_error do + if opts[:return_headers] do + {:ok, stream, %{headers: headers}} + else + {:ok, stream} + end + end + end + + defp do_receive_data( + %{payload: %{stream_response_pid: pid}}, + request_type, + opts + ) + when request_type in [:client_stream, :unary] do + responses = pid |> StreamResponseProcess.build_stream() |> Enum.to_list() + + with :ok <- check_for_error(responses) do + data = Keyword.fetch!(responses, :ok) + + if opts[:return_headers] do + {:ok, data, get_headers_and_trailers(responses)} + else + {:ok, data} + end + end + end + + def handle_errors_receive_data(%GRPC.Client.Stream{payload: %{response: response}}, _opts) do + {:error, "Error occurred while receiving data: #{inspect(response)}"} + end + + defp success_response?(%GRPC.Client.Stream{ + payload: %{response: {:ok, _resp}} + }), + do: true + + defp success_response?(_stream), do: false + + defp do_request( + %{channel: %{adapter_payload: %{conn_pid: pid}}, path: path} = stream, + opts, + body + ) do + headers = GRPC.Transport.HTTP2.client_headers_without_reserved(stream, opts) + + {:ok, stream_response_pid} = + StreamResponseProcess.start_link(stream, return_headers_for_request?(stream, opts)) + + response = + ConnectionProcess.request(pid, "POST", path, headers, body, + stream_response_pid: stream_response_pid + ) + + stream + |> GRPC.Client.Stream.put_payload(:response, response) + |> GRPC.Client.Stream.put_payload(:stream_response_pid, stream_response_pid) + end + + defp get_headers_and_trailers(responses) do + %{headers: Keyword.get(responses, :headers), trailers: Keyword.get(responses, :trailers)} + end + + def check_for_error(responses) do + error = Keyword.get(responses, :error) + + if error, do: {:error, error}, else: :ok + end + + defp return_headers_for_request?(%GRPC.Client.Stream{grpc_type: type}, _opts) + when type in [:bidirectional_stream, :server_stream] do + true + end + + defp return_headers_for_request?(_stream, opts) do + # Explicitly check for true to ensure the boolean type here + opts[:return_headers] == true + end +end diff --git a/lib/grpc/client/adapters/mint/connection_process/connection_process.ex b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex new file mode 100644 index 000000000..d5255fefb --- /dev/null +++ b/lib/grpc/client/adapters/mint/connection_process/connection_process.ex @@ -0,0 +1,406 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcess do + @moduledoc false + + # This module is responsible for managing a connection with a gRPC server. + # It's also responsible for managing requests, which also includes checks for the + # connection/request window size, splitting a given payload into appropriate sized chunks + # and streaming those to the server using an internal queue. + + use GenServer + + alias GRPC.Client.Adapters.Mint.ConnectionProcess.State + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + + require Logger + + @connection_closed_error "the connection is closed" + + @doc """ + Starts and link connection process + """ + @spec start_link(Mint.Types.scheme(), Mint.Types.address(), :inet.port_number(), keyword()) :: + GenServer.on_start() + def start_link(scheme, host, port, opts \\ []) do + opts = Keyword.put(opts, :parent, self()) + GenServer.start_link(__MODULE__, {scheme, host, port, opts}) + end + + @doc """ + Sends a request to the connected server. + + ## Options + + * :stream_response_pid (required) - the process to where send the responses coming from the connection will be sent to be processed + """ + @spec request( + pid :: pid(), + method :: String.t(), + path :: String.t(), + Mint.Types.headers(), + body :: iodata() | nil | :stream, + opts :: keyword() + ) :: {:ok, %{request_ref: Mint.Types.request_ref()}} | {:error, Mint.Types.error()} + def request(pid, method, path, headers, body, opts \\ []) do + GenServer.call(pid, {:request, method, path, headers, body, opts}) + end + + @doc """ + Closes the given connection. + """ + @spec disconnect(pid :: pid()) :: :ok + def disconnect(pid) do + GenServer.call(pid, :disconnect) + end + + @doc """ + Streams a chunk of the request body on the connection or signals the end of the body. + """ + @spec stream_request_body( + pid(), + Mint.Types.request_ref(), + iodata() | :eof | {:eof, trailing_headers :: Mint.Types.headers()} + ) :: :ok | {:error, Mint.Types.error()} + def stream_request_body(pid, request_ref, body) do + GenServer.call(pid, {:stream_body, request_ref, body}) + end + + @doc """ + cancels an open request request + """ + @spec cancel(pid(), Mint.Types.request_ref()) :: :ok | {:error, Mint.Types.error()} + def cancel(pid, request_ref) do + GenServer.call(pid, {:cancel_request, request_ref}) + end + + ## Callbacks + + @impl true + def init({scheme, host, port, opts}) do + case Mint.HTTP.connect(scheme, host, port, opts) do + {:ok, conn} -> + {:ok, State.new(conn, opts[:parent])} + + {:error, reason} -> + Logger.error( + "unable to establish a connection to #{scheme}://#{host}:#{port}. reason: #{inspect(reason)}" + ) + + {:stop, reason} + end + catch + :exit, reason -> + Logger.error( + "unable to establish a connection to #{scheme}://#{host}:#{port}. reason: #{inspect(reason)}" + ) + + {:stop, reason} + end + + @impl true + def handle_call(:disconnect, _from, state) do + # TODO add a code to if disconnect is brutal we just stop if is friendly we wait for pending requests + {:ok, conn} = Mint.HTTP.close(state.conn) + {:stop, :normal, :ok, State.update_conn(state, conn)} + end + + def handle_call(_request, _from, %{conn: %Mint.HTTP2{state: :closed}} = state) do + {:reply, {:error, "the connection is closed"}, state} + end + + def handle_call( + {:request, method, path, headers, :stream, opts}, + _from, + state + ) do + case Mint.HTTP.request(state.conn, method, path, headers, :stream) do + {:ok, conn, request_ref} -> + new_state = + state + |> State.update_conn(conn) + |> State.put_empty_ref_state(request_ref, opts[:stream_response_pid]) + + {:reply, {:ok, %{request_ref: request_ref}}, new_state} + + {:error, conn, reason} -> + new_state = State.update_conn(state, conn) + {:reply, {:error, reason}, new_state} + end + end + + def handle_call( + {:request, method, path, headers, body, opts}, + _from, + state + ) do + case Mint.HTTP.request(state.conn, method, path, headers, :stream) do + {:ok, conn, request_ref} -> + queue = :queue.in({request_ref, body, nil}, state.request_stream_queue) + + new_state = + state + |> State.update_conn(conn) + |> State.update_request_stream_queue(queue) + |> State.put_empty_ref_state(request_ref, opts[:stream_response_pid]) + + {:reply, {:ok, %{request_ref: request_ref}}, new_state, + {:continue, :process_request_stream_queue}} + + {:error, conn, reason} -> + new_state = State.update_conn(state, conn) + {:reply, {:error, reason}, new_state} + end + end + + def handle_call({:stream_body, request_ref, :eof}, _from, state) do + case Mint.HTTP.stream_request_body(state.conn, request_ref, :eof) do + {:ok, conn} -> + {:reply, :ok, State.update_conn(state, conn)} + + {:error, conn, error} -> + {:reply, {:error, error}, State.update_conn(state, conn)} + end + end + + def handle_call({:stream_body, request_ref, body}, from, state) do + queue = :queue.in({request_ref, body, from}, state.request_stream_queue) + + {:noreply, State.update_request_stream_queue(state, queue), + {:continue, :process_request_stream_queue}} + end + + def handle_call({:cancel_request, request_ref}, _from, state) do + state = process_response({:done, request_ref}, state) + + case Mint.HTTP2.cancel_request(state.conn, request_ref) do + {:ok, conn} -> {:reply, :ok, State.update_conn(state, conn)} + {:error, conn, error} -> {:reply, {:error, error}, State.update_conn(state, conn)} + end + end + + @impl true + def handle_info(message, state) do + case Mint.HTTP.stream(state.conn, message) do + :unknown -> + Logger.debug(fn -> "Received unknown message: " <> inspect(message) end) + {:noreply, state} + + {:ok, conn, responses} -> + state = State.update_conn(state, conn) + + state = + case state.requests do + requests when map_size(requests) == 0 -> + state + + _ -> + Enum.reduce(responses, state, &process_response/2) + end + + check_connection_status(state) + + {:error, conn, _error, _responses} -> + state = State.update_conn(state, conn) + check_connection_status(state) + end + end + + @impl true + def handle_continue(:process_request_stream_queue, state) do + {{:value, request}, queue} = :queue.out(state.request_stream_queue) + {ref, body, _from} = request + window_size = get_window_size(state.conn, ref) + dequeued_state = State.update_request_stream_queue(state, queue) + + cond do + # Do nothing, wait for server (on stream/2) to give us more window size + window_size == 0 -> {:noreply, state} + IO.iodata_length(body) > window_size -> chunk_body_and_enqueue_rest(request, dequeued_state) + true -> stream_body_and_reply(request, dequeued_state) + end + end + + @impl true + def terminate(_reason, _state) do + :normal + end + + defp process_response({:status, request_ref, status}, state) do + State.update_response_status(state, request_ref, status) + end + + defp process_response({:headers, request_ref, headers}, state) do + if State.empty_headers?(state, request_ref) do + new_state = State.update_response_headers(state, request_ref, headers) + + :ok = + new_state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:headers, headers) + + new_state + else + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:trailers, headers) + + state + end + end + + defp process_response({:data, request_ref, new_data}, state) do + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:data, new_data) + + state + end + + defp process_response({:done, request_ref}, state) do + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.done() + + {_ref, new_state} = State.pop_ref(state, request_ref) + new_state + end + + defp chunk_body_and_enqueue_rest({request_ref, body, from}, state) do + {head, tail} = chunk_body(body, get_window_size(state.conn, request_ref)) + + case Mint.HTTP.stream_request_body(state.conn, request_ref, head) do + {:ok, conn} -> + queue = :queue.in_r({request_ref, tail, from}, state.request_stream_queue) + + new_state = + state + |> State.update_conn(conn) + |> State.update_request_stream_queue(queue) + + {:noreply, new_state} + + {:error, conn, error} -> + if from != nil do + # We need an explicit reply here because the process that called this GenServer + # isn't the same one that's expecting the reply. + GenServer.reply(from, {:error, error}) + else + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) + end + + {:noreply, State.update_conn(state, conn)} + end + end + + defp stream_body_and_reply({request_ref, body, from}, state) do + send_eof? = is_nil(from) + + case stream_body(state.conn, request_ref, body, send_eof?) do + {:ok, conn} -> + if not send_eof? do + GenServer.reply(from, :ok) + end + + check_request_stream_queue(State.update_conn(state, conn)) + + {:error, conn, error} -> + if not send_eof? do + GenServer.reply(from, {:error, error}) + else + :ok = + state + |> State.stream_response_pid(request_ref) + |> StreamResponseProcess.consume(:error, error) + end + + check_request_stream_queue(State.update_conn(state, conn)) + end + end + + defp stream_body(conn, request_ref, body, true = _stream_eof?) do + case Mint.HTTP.stream_request_body(conn, request_ref, body) do + {:ok, conn} -> Mint.HTTP.stream_request_body(conn, request_ref, :eof) + error -> error + end + end + + defp stream_body(conn, request_ref, body, false = _stream_eof?) do + Mint.HTTP.stream_request_body(conn, request_ref, body) + end + + def check_request_stream_queue(state) do + if :queue.is_empty(state.request_stream_queue) do + {:noreply, state} + else + {:noreply, state, {:continue, :process_request_stream_queue}} + end + end + + defp chunk_body(body, bytes_length) do + case body do + <> -> {head, tail} + _other -> {body, <<>>} + end + end + + def get_window_size(conn, ref) do + min( + Mint.HTTP2.get_window_size(conn, {:request, ref}), + Mint.HTTP2.get_window_size(conn, :connection) + ) + end + + defp finish_all_pending_requests(state) do + new_state = + state.request_stream_queue + |> :queue.to_list() + |> Enum.reduce(state, fn {request_ref, _, _} = request, acc_state -> + case request do + {ref, _body, nil} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + {ref, _body, from} -> + acc_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + + GenServer.reply(from, {:error, @connection_closed_error}) + end + + {_request_data, new_state} = State.pop_ref(acc_state, request_ref) + new_state + end) + + # Inform the parent that the connection is down + send(new_state.parent, {:elixir_grpc, :connection_down, self()}) + + new_state.requests + |> Enum.each(fn {ref, _} -> + new_state + |> State.stream_response_pid(ref) + |> send_connection_close_and_end_stream_response() + end) + + {:noreply, State.update_request_stream_queue(%{new_state | requests: %{}}, :queue.new())} + end + + defp send_connection_close_and_end_stream_response(pid) do + :ok = StreamResponseProcess.consume(pid, :error, @connection_closed_error) + :ok = StreamResponseProcess.done(pid) + end + + defp check_connection_status(state) do + if Mint.HTTP.open?(state.conn) do + check_request_stream_queue(state) + else + finish_all_pending_requests(state) + end + end +end diff --git a/lib/grpc/client/adapters/mint/connection_process/state.ex b/lib/grpc/client/adapters/mint/connection_process/state.ex new file mode 100644 index 000000000..bad73233d --- /dev/null +++ b/lib/grpc/client/adapters/mint/connection_process/state.ex @@ -0,0 +1,55 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcess.State do + @moduledoc false + + defstruct [:conn, :parent, requests: %{}, request_stream_queue: :queue.new()] + + @type t :: %__MODULE__{ + conn: Mint.HTTP.t(), + requests: map(), + parent: pid() + } + + def new(conn, parent) do + %__MODULE__{conn: conn, request_stream_queue: :queue.new(), parent: parent} + end + + def update_conn(state, conn) do + %{state | conn: conn} + end + + def update_request_stream_queue(state, queue) do + %{state | request_stream_queue: queue} + end + + def put_empty_ref_state(state, ref, response_pid) do + put_in(state.requests[ref], %{ + stream_response_pid: response_pid, + done: false, + response: %{} + }) + end + + def update_response_status(state, ref, status) do + put_in(state.requests[ref].response[:status], status) + end + + def update_response_headers(state, ref, headers) do + put_in(state.requests[ref].response[:headers], headers) + end + + def empty_headers?(state, ref) do + is_nil(state.requests[ref].response[:headers]) + end + + def stream_response_pid(state, ref) do + state.requests[ref].stream_response_pid + end + + def pop_ref(state, ref) do + pop_in(state.requests[ref]) + end + + def append_response_data(state, ref, new_data) do + update_in(state.requests[ref].response[:data], fn data -> (data || "") <> new_data end) + end +end diff --git a/lib/grpc/client/adapters/mint/stream_response_process.ex b/lib/grpc/client/adapters/mint/stream_response_process.ex new file mode 100644 index 000000000..09cf2b434 --- /dev/null +++ b/lib/grpc/client/adapters/mint/stream_response_process.ex @@ -0,0 +1,211 @@ +defmodule GRPC.Client.Adapters.Mint.StreamResponseProcess do + @moduledoc false + # This module represents the process responsible for consuming the + # incoming messages from a connection. For each request, there will be + # a process responsible for consuming its messages. At the end of a stream + # this process will automatically be killed. + + # TODO: Refactor the GenServer.call/3 occurrences on this module to produce + # telemetry events and log entries in case of failures + + @typep accepted_types :: :data | :trailers | :headers | :error + @typep data_types :: binary() | Mint.Types.headers() | Mint.Types.error() + + @accepted_types [:data, :trailers, :headers, :error] + @header_types [:headers, :trailers] + + use GenServer + + @spec start_link(GRPC.Client.Stream.t(), send_headers_or_trailers? :: boolean()) :: + GenServer.on_start() + def start_link(stream, send_headers_or_trailers?) do + GenServer.start_link(__MODULE__, {stream, send_headers_or_trailers?}) + end + + @doc """ + Given a pid from this process, build an Elixir.Stream that will consume the accumulated + data inside this process + """ + @spec build_stream(pid(), produce_trailers? :: boolean) :: Enumerable.t() + def build_stream(pid, produce_trailers? \\ true) do + Stream.unfold(pid, fn pid -> + pid + |> GenServer.call(:get_response, :infinity) + |> process_response(produce_trailers?, pid) + end) + end + + defp process_response(nil = _response, _produce_trailers, _pid), do: nil + + defp process_response({:trailers, _trailers}, false = produce_trailers?, pid) do + pid + |> GenServer.call(:get_response, :infinity) + |> process_response(produce_trailers?, pid) + end + + defp process_response(response, _produce_trailers, pid) do + {response, pid} + end + + @doc """ + Cast a message to process to inform that the stream has finished + once all messages are produced. This process will automatically + be killed. + """ + @spec done(pid()) :: :ok + def done(pid) do + :ok = GenServer.call(pid, {:consume_response, :done}) + :ok + end + + @doc """ + Consume an incoming data or trailers/headers + """ + @spec consume(pid(), type :: accepted_types, data :: data_types) :: :ok + def consume(pid, type, data) when type in @accepted_types do + :ok = GenServer.call(pid, {:consume_response, {type, data}}) + :ok + end + + # Callbacks + + @impl true + def init({stream, send_headers_or_trailers?}) do + state = %{ + grpc_stream: stream, + send_headers_or_trailers: send_headers_or_trailers?, + buffer: <<>>, + responses: :queue.new(), + done: false, + from: nil, + compressor: nil + } + + {:ok, state} + end + + @impl true + def handle_call(:get_response, from, state) do + {:noreply, put_in(state[:from], from), {:continue, :produce_response}} + end + + def handle_call({:consume_response, {:data, data}}, _from, state) do + %{ + buffer: buffer, + grpc_stream: %{response_mod: res_mod, codec: codec}, + responses: responses + } = state + + case GRPC.Message.get_message(buffer <> data, state.compressor) do + {{_, message}, rest} -> + # TODO add code here to handle compressor headers + response = codec.decode(message, res_mod) + new_responses = :queue.in({:ok, response}, responses) + new_state = %{state | buffer: rest, responses: new_responses} + {:reply, :ok, new_state, {:continue, :produce_response}} + + _ -> + new_state = %{state | buffer: buffer <> data} + {:reply, :ok, new_state, {:continue, :produce_response}} + end + end + + def handle_call( + {:consume_response, {type, headers}}, + _from, + %{send_headers_or_trailers: true, responses: responses} = state + ) + when type in @header_types do + state = update_compressor({type, headers}, state) + new_responses = :queue.in(get_headers_response(headers, type), responses) + {:reply, :ok, %{state | responses: new_responses}, {:continue, :produce_response}} + end + + def handle_call( + {:consume_response, {type, headers}}, + _from, + %{send_headers_or_trailers: false, responses: responses} = state + ) + when type in @header_types do + state = update_compressor({type, headers}, state) + + case get_headers_response(headers, type) do + {:error, _rpc_error} = error -> + {:reply, :ok, %{state | responses: :queue.in(error, responses)}, + {:continue, :produce_response}} + + _any -> + {:reply, :ok, state, {:continue, :produce_response}} + end + end + + def handle_call( + {:consume_response, {:error, _error} = error}, + _from, + %{responses: responses} = state + ) do + {:reply, :ok, %{state | responses: :queue.in(error, responses)}, + {:continue, :produce_response}} + end + + def handle_call({:consume_response, :done}, _from, state) do + {:reply, :ok, %{state | done: true}, {:continue, :produce_response}} + end + + @impl true + def handle_continue(:produce_response, state) do + no_responses? = :queue.is_empty(state.responses) + without_from? = is_nil(state.from) + + cond do + without_from? -> + {:noreply, state} + + no_responses? and state.done -> + GenServer.reply(state.from, nil) + {:stop, :normal, state} + + no_responses? -> + {:noreply, state} + + true -> + {{:value, response}, rest} = :queue.out(state.responses) + GenServer.reply(state.from, response) + {:noreply, %{state | responses: rest, from: nil}} + end + end + + defp get_headers_response(headers, type) do + decoded_trailers = GRPC.Transport.HTTP2.decode_headers(headers) + status = String.to_integer(decoded_trailers["grpc-status"] || "0") + + if status == GRPC.Status.ok() do + {type, decoded_trailers} + else + rpc_error = %GRPC.RPCError{status: status, message: decoded_trailers["grpc-message"]} + {:error, rpc_error} + end + end + + defp update_compressor({:headers, headers}, state) do + decoded_trailers = GRPC.Transport.HTTP2.decode_headers(headers) + + compressor = + get_compressor(decoded_trailers["grpc-encoding"], state.grpc_stream.accepted_compressors) + + %{state | compressor: compressor} + end + + defp update_compressor(_headers, state), do: state + + defp get_compressor(nil = _encoding_name, _accepted_compressors), do: nil + + defp get_compressor(encoding_name, accepted_compressors) do + Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding_name end) + end + + @impl true + def terminate(_reason, _state) do + :normal + end +end diff --git a/lib/grpc/client/interceptor.ex b/lib/grpc/client/interceptor.ex new file mode 100644 index 000000000..a2d3b7e44 --- /dev/null +++ b/lib/grpc/client/interceptor.ex @@ -0,0 +1,30 @@ +defmodule GRPC.ClientInterceptor do + @moduledoc """ + Interceptor on client side. See `GRPC.Stub.connect/2`. + """ + + @moduledoc deprecated: "Use `GRPC.Client.Interceptor` instead" + + alias GRPC.Client.Stream + + @type options :: any() + @type req :: struct() | nil + @type next :: (Stream.t(), req -> GRPC.Stub.rpc_return()) + + @callback init(options) :: options + @callback call(stream :: Stream.t(), req, next, options) :: GRPC.Stub.rpc_return() +end + +defmodule GRPC.Client.Interceptor do + @moduledoc """ + Interceptor on client side. See `GRPC.Stub.connect/2`. + """ + alias GRPC.Client.Stream + + @type options :: any() + @type req :: struct() | nil + @type next :: (Stream.t(), req -> GRPC.Stub.rpc_return()) + + @callback init(options) :: options + @callback call(stream :: Stream.t(), req, next, options) :: GRPC.Stub.rpc_return() +end diff --git a/lib/grpc/client/interceptors/logger.ex b/lib/grpc/client/interceptors/logger.ex new file mode 100644 index 000000000..48e523147 --- /dev/null +++ b/lib/grpc/client/interceptors/logger.ex @@ -0,0 +1,104 @@ +defmodule GRPC.Client.Interceptors.Logger do + @moduledoc """ + Print log around client rpc calls, like + + 17:13:33.021 [info] Call helloworld.Greeter.say_hello -> :ok (58 ms) + 17:13:33.021 [error] Call helloworld.Greeter.say_hello -> %GRPC.RPCError{status: 3, message: "Invalid argument"} (58 ms) + + ## Options + + * `:level` - the desired log level. Defaults to `:info` + + ## Usage + + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Client.Interceptors.Logger]) + + ## Usage with custom level + + {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Client.Interceptors.Logger, level: :warning}]) + """ + + @behaviour GRPC.Client.Interceptor + + require Logger + + @impl GRPC.Client.Interceptor + def init(opts) do + level = Keyword.get(opts, :level) || :info + [level: level] + end + + @impl GRPC.Client.Interceptor + def call(%{grpc_type: grpc_type} = stream, req, next, opts) do + level = Keyword.fetch!(opts, :level) + + if Logger.compare_levels(level, Logger.level()) != :lt do + start = System.monotonic_time() + + try do + result = next.(stream, req) + stop = System.monotonic_time() + + log_result(result, level, grpc_type, stream, start, stop) + result + rescue + error in GRPC.RPCError -> + stop = System.monotonic_time() + log_error(error, stream, start, stop) + + reraise error, __STACKTRACE__ + end + else + next.(stream, req) + end + end + + defp log_error(error, stream, start, stop) do + diff = System.convert_time_unit(stop - start, :native, :microsecond) + + Logger.log(:error, fn -> + [ + "Call ", + stream.service_name, + ".", + to_string(elem(stream.rpc, 0)), + " -> ", + inspect(error), + " (", + formatted_diff(diff), + ")" + ] + end) + end + + defp log_result(result, level, grpc_type, stream, start, stop) do + case grpc_type do + :unary -> + status = elem(result, 0) + + diff = System.convert_time_unit(stop - start, :native, :microsecond) + + Logger.log(level, fn -> + [ + "Call ", + stream.service_name, + ".", + to_string(elem(stream.rpc, 0)), + " -> ", + inspect(status), + " (", + formatted_diff(diff), + ")" + ] + end) + + _otherwise -> + Logger.log(level, fn -> + ["Call ", to_string(elem(stream.rpc, 0)), " of ", stream.service_name] + end) + end + end + + def formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] + def formatted_diff(diff), do: [Integer.to_string(diff), "µs"] +end diff --git a/lib/grpc/client/stream.ex b/lib/grpc/client/stream.ex index c75e6f397..107e0c4b8 100644 --- a/lib/grpc/client/stream.ex +++ b/lib/grpc/client/stream.ex @@ -7,31 +7,31 @@ defmodule GRPC.Client.Stream do * `:channel` - `GRPC.Channel`, the channel established by client * `:payload` - data used by adapter in a request * `:path` - the request path to sent - * `request_mod` - the request module, or nil for untyped protocols - * `response_mod` - the response module, or nil for untyped protocols + * `:request_mod` - the request module, or nil for untyped protocols + * `:response_mod` - the response module, or nil for untyped protocols * `:codec` - the codec * `:req_stream` - indicates if request is streaming * `:res_stream` - indicates if reply is streaming """ - @typep stream_payload :: any + @typep stream_payload :: any() @type t :: %__MODULE__{ channel: GRPC.Channel.t(), service_name: String.t(), method_name: String.t(), - grpc_type: atom, - rpc: tuple, + grpc_type: atom(), + rpc: tuple(), payload: stream_payload, path: String.t(), - request_mod: atom, - response_mod: atom, - codec: atom, - server_stream: boolean, - canceled: boolean, - compressor: module, - accepted_compressors: [module], - headers: map, - __interface__: map + request_mod: atom(), + response_mod: atom(), + codec: atom(), + server_stream: boolean(), + canceled: boolean(), + compressor: module(), + accepted_compressors: [module()], + headers: map(), + __interface__: map() } defstruct channel: nil, @@ -50,7 +50,10 @@ defmodule GRPC.Client.Stream do compressor: nil, accepted_compressors: [], headers: %{}, - __interface__: %{send_request: &__MODULE__.send_request/3, recv: &GRPC.Stub.do_recv/2} + __interface__: %{ + send_request: &__MODULE__.send_request/3, + receive_data: &__MODULE__.receive_data/2 + } @doc false def put_payload(%{payload: payload} = stream, key, val) do @@ -75,7 +78,7 @@ defmodule GRPC.Client.Stream do opts ) do encoded = codec.encode(request) - send_end_stream = Keyword.get(opts, :end_stream, false) + send_end_stream = Keyword.get(opts, :end_stream) || false # If compressor exists, compress is true by default compressor = @@ -90,4 +93,8 @@ defmodule GRPC.Client.Stream do compressor: compressor ) end + + def receive_data(%{channel: %{adapter: adapter}} = stream, opts) do + adapter.receive_data(stream, opts) + end end diff --git a/lib/grpc/codec.ex b/lib/grpc/codec.ex index ce4a2a991..e052a95eb 100644 --- a/lib/grpc/codec.ex +++ b/lib/grpc/codec.ex @@ -5,6 +5,25 @@ defmodule GRPC.Codec do @doc ~s(Name is identity of the codec, which will be suffix after content-type "application/grpc+" such as "proto".) @callback name() :: String.t() - @callback encode(any) :: binary + @callback encode(any) :: iodata() @callback decode(any, module :: atom) :: any + + @doc """ + This function is invoked before the gRPC payload is transformed into a protobuf message whenever it is defined. + + This can be used to apply a transform over the gRPC message before decoding it. For instance grpc-web using the `application/grpc-web-text` + content type requires the message to be Base64-encoded, so a server receving messages using grpc-web-text will be required to + do a Base64 decode on the payload before decoding the gRPC message. + """ + @callback unpack_from_channel(binary) :: binary + + @doc """ + This function is invoked whenever it is defined after the protobuf message has been transformed into a gRPC payload. + + This can be used to apply a transform over the gRPC message before sending it. + For instance grpc-web using the `application/grpc-web-text` content type requires the message to be Base64-encoded, so a server sending messages using grpc-web-text will be required to + do a Base64 encode on the payload before sending the gRPC message. + """ + @callback pack_for_channel(iodata()) :: binary + @optional_callbacks unpack_from_channel: 1, pack_for_channel: 1 end diff --git a/lib/grpc/codec/json.ex b/lib/grpc/codec/json.ex new file mode 100644 index 000000000..b82b455d1 --- /dev/null +++ b/lib/grpc/codec/json.ex @@ -0,0 +1,73 @@ +if Code.ensure_loaded?(Jason) do + defmodule GRPC.Codec.JSON do + @moduledoc """ + JSON Codec for gRPC communication. + + This module implements the `GRPC.Codec` behaviour, providing encoding and decoding functions + for JSON serialization in the context of gRPC communication. + + ## Behavior Functions + + - `name/0`: Returns the name of the codec, which is "json". + - `encode/1`: Encodes a struct using the Protobuf.JSON.encode!/1 function. + - `decode/2`: Decodes binary data into a map using the Jason library. + + This module requires the Jason dependency. + """ + @behaviour GRPC.Codec + + def name(), do: "json" + + @doc """ + Encodes a struct using the Protobuf.JSON.encode!/1 function. + + ### Parameters: + + - `struct` - The struct to be encoded. + + ### Returns: + + The encoded binary data. + + ### Example: + + ```elixir + %MyStruct{id: 1, name: "John"} |> GRPC.Codec.JSON.encode() + ``` + + """ + + def encode(struct) do + Protobuf.JSON.encode!(struct) + end + + @doc """ + Decodes binary data into a map using the Jason library. + Parameters: + + binary - The binary data to be decoded. + module - Module to be created. + + Returns: + + A map representing the decoded data. + + Raises: + + Raises an error if the Jason library is not loaded. + + Example: + + ```elixir + binary_data |> GRPC.Codec.JSON.decode(__MODULE__) + ``` + """ + def decode(<<>>, _module), do: %{} + + def decode(binary, _module), do: Jason.decode!(binary) + end +else + defmodule GRPC.Codec.JSON do + def decode(_, _), do: raise(ArgumentError, "Module Jason not found") + end +end diff --git a/lib/grpc/codec/proto.ex b/lib/grpc/codec/proto.ex index d3a9748a4..db89922ff 100644 --- a/lib/grpc/codec/proto.ex +++ b/lib/grpc/codec/proto.ex @@ -6,10 +6,10 @@ defmodule GRPC.Codec.Proto do end def encode(struct) do - Protobuf.Encoder.encode(struct) + Protobuf.Encoder.encode_to_iodata(struct) end def decode(binary, module) do - Protobuf.Decoder.decode(binary, module) + module.decode(binary) end end diff --git a/lib/grpc/codec/web_text.ex b/lib/grpc/codec/web_text.ex new file mode 100644 index 000000000..398e98564 --- /dev/null +++ b/lib/grpc/codec/web_text.ex @@ -0,0 +1,29 @@ +defmodule GRPC.Codec.WebText do + @behaviour GRPC.Codec + + def name() do + "text" + end + + def encode(struct) do + Protobuf.Encoder.encode(struct) + end + + def pack_for_channel(data) when is_list(data) do + data + |> IO.iodata_to_binary() + |> Base.encode64() + end + + def pack_for_channel(binary) do + Base.encode64(binary) + end + + def unpack_from_channel(binary) do + Base.decode64!(binary) + end + + def decode(binary, module) do + Protobuf.Decoder.decode(binary, module) + end +end diff --git a/lib/grpc/compressor.ex b/lib/grpc/compressor.ex index ca0b2439a..37abc2e4e 100644 --- a/lib/grpc/compressor.ex +++ b/lib/grpc/compressor.ex @@ -1,5 +1,5 @@ defmodule GRPC.Compressor do @callback name() :: String.t() - @callback compress(binary) :: binary + @callback compress(iodata()) :: binary @callback decompress(binary) :: binary end diff --git a/lib/grpc/credential.ex b/lib/grpc/credential.ex index e9eebe0da..0205d909e 100644 --- a/lib/grpc/credential.ex +++ b/lib/grpc/credential.ex @@ -1,8 +1,15 @@ defmodule GRPC.Credential do @moduledoc """ - Stores credentials for authentication. It can be used to establish secure connections + Stores credentials for authentication. + + It can be used to establish secure connections by passed to `GRPC.Stub.connect/2` as an argument. + Some client and server adapter implementations may + choose to let request options override some of the + configuration here, but this is left as a choice + for each adapter. + ## Examples iex> cred = GRPC.Credential.new(ssl: [cacertfile: ca_path]) @@ -10,12 +17,13 @@ defmodule GRPC.Credential do """ @type t :: %__MODULE__{ssl: [:ssl.tls_option()]} - defstruct [:ssl] + defstruct ssl: [] @doc """ Creates credential. """ def new(opts) do - %__MODULE__{ssl: Keyword.get(opts, :ssl, [])} + opts = Keyword.validate!(opts, [:ssl]) + %__MODULE__{ssl: opts[:ssl] || []} end end diff --git a/lib/grpc/endpoint.ex b/lib/grpc/endpoint.ex index a51cfb2d1..c7b7fb57c 100644 --- a/lib/grpc/endpoint.ex +++ b/lib/grpc/endpoint.ex @@ -7,15 +7,15 @@ defmodule GRPC.Endpoint do defmodule Your.Endpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server, level: :info + intercept GRPC.Server.Interceptors.Logger, level: :info intercept Other.Interceptor run HelloServer, interceptors: [HelloHaltInterceptor] run FeatureServer end Interceptors will be run around your rpc calls from top to bottom. And you can even set - interceptors for some of servers. In the above example, `[GRPC.Logger.Server, Other.Interceptor, - HelloHaltInterceptor]` will be run for `HelloServer`, and `[GRPC.Logger.Server, Other.Interceptor]` + interceptors for some of servers. In the above example, `[GRPC.Server.Interceptors.Logger, Other.Interceptor, + HelloHaltInterceptor]` will be run for `HelloServer`, and `[GRPC.Server.Interceptors.Logger, Other.Interceptor]` will be run for `FeatureServer`. """ @@ -39,7 +39,21 @@ defmodule GRPC.Endpoint do |> init_interceptors() servers = Module.get_attribute(env.module, :servers) - servers = Enum.map(servers, fn {ss, opts} -> {ss, parse_run_opts(opts, %{})} end) + + servers = + Enum.map(servers, fn {ss, opts} -> + opts = Keyword.validate!(opts, [:interceptors]) + + run_args = + if interceptors = opts[:interceptors] do + %{interceptors: interceptors} + else + %{} + end + + {ss, run_args} + end) + server_interceptors = server_interceptors(servers, %{}) servers = parse_servers(servers) @@ -100,16 +114,6 @@ defmodule GRPC.Endpoint do |> List.flatten() end - defp parse_run_opts([], acc), do: acc - - defp parse_run_opts([{:interceptors, interceptors} | tail], acc) do - parse_run_opts(tail, Map.put(acc, :interceptors, interceptors)) - end - - defp parse_run_opts([{k, _} | _], _) do - raise ArgumentError, message: "Unknown option for GRPC.Endpoint.run/2: #{k}" - end - defp init_interceptors(interceptors) do Enum.map(interceptors, fn {interceptor, opts} -> diff --git a/lib/grpc/error.ex b/lib/grpc/error.ex deleted file mode 100644 index 3d8c70503..000000000 --- a/lib/grpc/error.ex +++ /dev/null @@ -1,101 +0,0 @@ -defmodule GRPC.RPCError do - @moduledoc """ - The RPC error raised in server side and got in client side. - - # server side - raise GRPC.RPCError, status: :unknown # preferred - raise GRPC.RPCError, status: GRPC.Status.unknown, message: "error message" - - # client side - {:error, error} = Your.Stub.unary_call(channel, request) - """ - - defexception [:status, :message, :details] - @type t :: %__MODULE__{status: GRPC.Status.t(), message: String.t(), details: list()} - - alias GRPC.Status - - @spec exception(Status.t(), String.t()) :: t - def new(status) when is_atom(status) do - exception(status: status) - end - - def exception(args) when is_list(args) do - error = parse_args(args, %__MODULE__{}) - - if error.message do - error - else - Map.put(error, :message, status_message(error.status)) - end - end - - defp parse_args([], acc), do: acc - - defp parse_args([{:status, status} | t], acc) when is_integer(status) do - acc = %{acc | status: status} - parse_args(t, acc) - end - - defp parse_args([{:status, status} | t], acc) when is_atom(status) do - acc = %{acc | status: apply(GRPC.Status, status, [])} - parse_args(t, acc) - end - - defp parse_args([{:message, message} | t], acc) when is_binary(message) do - acc = %{acc | message: message} - parse_args(t, acc) - end - - defp parse_args([{:details, details} | t], acc) when is_list(details) do - acc = %{acc | details: details} - parse_args(t, acc) - end - - def exception(status, message) when is_atom(status) do - %GRPC.RPCError{status: apply(GRPC.Status, status, []), message: message} - end - - def exception(status, message) when is_integer(status) do - %GRPC.RPCError{status: status, message: message} - end - - def exception(status, message, details) when is_atom(status) do - %GRPC.RPCError{status: apply(GRPC.Status, status, []), message: message, details: details} - end - - def exception(status, message, details) when is_integer(status) do - %GRPC.RPCError{status: status, message: message, details: details} - end - - defp status_message(1), do: "The operation was cancelled (typically by the caller)" - defp status_message(2), do: "Unknown error" - defp status_message(3), do: "Client specified an invalid argument" - defp status_message(4), do: "Deadline expired before operation could complete" - defp status_message(5), do: "Some requested entity (e.g., file or directory) was not found" - - defp status_message(6), - do: "Some entity that we attempted to create (e.g., file or directory) already exists" - - defp status_message(7), - do: "The caller does not have permission to execute the specified operation" - - defp status_message(8), do: "Some resource has been exhausted" - - defp status_message(9), - do: - "Operation was rejected because the system is not in a state required for the operation's execution" - - defp status_message(10), do: "The operation was aborted" - defp status_message(11), do: "Operation was attempted past the valid range" - - defp status_message(12), - do: "Operation is not implemented or not supported/enabled in this service" - - defp status_message(13), do: "Internal errors" - defp status_message(14), do: "The service is currently unavailable" - defp status_message(15), do: "Unrecoverable data loss or corruption" - - defp status_message(16), - do: "The request does not have valid authentication credentials for the operation" -end diff --git a/lib/grpc/interceptor.ex b/lib/grpc/interceptor.ex deleted file mode 100644 index 4e4c1555d..000000000 --- a/lib/grpc/interceptor.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule GRPC.ServerInterceptor do - @moduledoc """ - Interceptor on server side. See `GRPC.Endpoint`. - """ - alias GRPC.Server.Stream - - @type options :: any - @type rpc_return :: {:ok, Stream.t(), struct} | {:ok, Stream.t()} | {:error, GRPC.RPCError.t()} - @type next :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return) - - @callback init(options) :: options - @callback call(GRPC.Server.rpc_req(), stream :: Stream.t(), next, options) :: rpc_return -end - -defmodule GRPC.ClientInterceptor do - @moduledoc """ - Interceptor on client side. See `GRPC.Stub.connect/2`. - """ - alias GRPC.Client.Stream - - @type options :: any - @type req :: struct | nil - @type next :: (Stream.t(), req -> GRPC.Stub.rpc_return()) - - @callback init(options) :: options - @callback call(stream :: Stream.t(), req, next, options) :: GRPC.Stub.rpc_return() -end diff --git a/lib/grpc/logger.ex b/lib/grpc/logger.ex index 0c1e9b03c..eff6f27e7 100644 --- a/lib/grpc/logger.ex +++ b/lib/grpc/logger.ex @@ -1,95 +1,9 @@ -defmodule GRPC.Logger.Server do - @moduledoc """ - Print log around server rpc calls, like: - - 17:18:45.151 [info] Handled by HelloServer.say_hello - 17:18:45.151 [info] Response :ok in 11µs - - ## Usage - - defmodule Your.Endpoint do - use GRPC.Endpoint - - intercept GRPC.Logger.Server, level: :info - end +defmodule GRPC.Logger do + @doc """ + Normalizes the exception and stacktrace inputs by its kind to match the format specified for `crash_report` metadata + in [Logger](https://hexdocs.pm/logger/main/Logger.html#module-metadata) """ - require Logger - @behaviour GRPC.ServerInterceptor - - def init(opts) do - Keyword.get(opts, :level, :info) - end - - def call(req, stream, next, level) do - if Logger.compare_levels(level, Logger.level()) != :lt do - Logger.log(level, fn -> - ["Handled by ", inspect(stream.server), ".", to_string(elem(stream.rpc, 0))] - end) - - start = System.monotonic_time() - result = next.(req, stream) - stop = System.monotonic_time() - - status = elem(result, 0) - - Logger.log(level, fn -> - diff = System.convert_time_unit(stop - start, :native, :microsecond) - - ["Response ", inspect(status), " in ", formatted_diff(diff)] - end) - - result - else - next.(req, stream) - end - end - - def formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] - def formatted_diff(diff), do: [Integer.to_string(diff), "µs"] -end - -defmodule GRPC.Logger.Client do - require Logger - - @moduledoc """ - Print log around client rpc calls, like - - 17:13:33.021 [info] Call say_hello of helloworld.Greeter - 17:13:33.079 [info] Got :ok in 58ms - - ## Usage - - {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [GRPC.Logger.Client]) - {:ok, channel} = GRPC.Stub.connect("localhost:50051", interceptors: [{GRPC.Logger.Client, level: :info}]) - """ - - def init(opts) do - Keyword.get(opts, :level, :info) - end - - def call(%{grpc_type: grpc_type} = stream, req, next, level) do - if Logger.compare_levels(level, Logger.level()) != :lt do - Logger.log(level, fn -> - ["Call ", to_string(elem(stream.rpc, 0)), " of ", stream.service_name] - end) - - start = System.monotonic_time() - result = next.(stream, req) - stop = System.monotonic_time() - - if grpc_type == :unary do - status = elem(result, 0) - - Logger.log(level, fn -> - diff = System.convert_time_unit(stop - start, :native, :microsecond) - - ["Got ", inspect(status), " in ", GRPC.Logger.Server.formatted_diff(diff)] - end) - end - - result - else - next.(stream, req) - end - end + def crash_reason(:throw, reason, stacktrace), do: {{:nocatch, reason}, stacktrace} + def crash_reason(:error, reason, stack), do: {Exception.normalize(:error, reason, stack), stack} + def crash_reason(:exit, reason, stacktrace), do: {reason, stacktrace} end diff --git a/lib/grpc/message.ex b/lib/grpc/message.ex index ba908c7ae..03ac2e339 100644 --- a/lib/grpc/message.ex +++ b/lib/grpc/message.ex @@ -1,37 +1,51 @@ defmodule GRPC.Message do - use Bitwise, only_operators: true - @max_message_length 1 <<< (32 - 1) + @moduledoc """ + Transform data between encoded protobuf and HTTP/2 body of gRPC. - alias GRPC.RPCError + gRPC body format is: + + http://www.grpc.io/docs/guides/wire.html + Delimited-Message -> Compressed-Flag Message-Length Message + Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer + Message-Length -> {length of Message} # encoded as 4 byte unsigned integer + Message -> *{binary octet} + """ - @moduledoc false + alias GRPC.RPCError - # Transform data between encoded protobuf and HTTP/2 body of gRPC. - # - # gRPC body format is: - # - # # http://www.grpc.io/docs/guides/wire.html - # Delimited-Message -> Compressed-Flag Message-Length Message - # Compressed-Flag -> 0 / 1 # encoded as 1 byte unsigned integer - # Message-Length -> {length of Message} # encoded as 4 byte unsigned integer - # Message -> *{binary octet} + @max_message_length Bitwise.bsl(1, 32 - 1) @doc """ - Transform protobuf data to gRPC body + Transforms Protobuf data into a gRPC body binary. + + ## Options + + * `:compressor` - the optional `GRPC.Compressor` to be used. + * `:iolist` - if `true`, encodes the data as an `t:iolist()` instead of a `t:binary()` + * `:max_message_length` - the maximum number of bytes for the encoded message. ## Examples - iex> message = <<1, 2, 3, 4, 5, 6, 7, 8>> + iex> message = ["m", [["es", "sa"], "ge"]] iex> GRPC.Message.to_data(message) - {:ok, <<0, 0, 0, 0, 8, 1, 2, 3, 4, 5, 6, 7, 8>>, 13} + {:ok, <<0, 0, 0, 0, 7, "message">>, 12} + iex> GRPC.Message.to_data(message, iolist: true) + {:ok, [0, <<0, 0, 0, 7>>, ["m", [["es", "sa"], "ge"]]], 12} + + Error cases: + iex> message = <<1, 2, 3, 4, 5, 6, 7, 8, 9>> iex> GRPC.Message.to_data(message, %{max_message_length: 8}) {:error, "Encoded message is too large (9 bytes)"} + """ - @spec to_data(iodata, map | Keyword.t()) :: - {:ok, binary, non_neg_integer} | {:error, String.t()} - def to_data(message, opts \\ %{}) do + @spec to_data(iodata, keyword()) :: + {:ok, iodata, non_neg_integer} | {:error, String.t()} + def to_data(message, opts \\ []) do compressor = opts[:compressor] + iolist = opts[:iolist] + codec = opts[:codec] + max_length = opts[:max_message_length] || @max_message_length {compress_flag, message} = if compressor do @@ -40,19 +54,26 @@ defmodule GRPC.Message do {0, message} end - length = byte_size(message) - max_length = opts[:max_message_length] || @max_message_length + length = IO.iodata_length(message) if length > max_length do {:error, "Encoded message is too large (#{length} bytes)"} else - result = <> + result = [compress_flag, <>, message] + + result = + if function_exported?(codec, :pack_for_channel, 1), + do: codec.pack_for_channel(result), + else: result + + result = if iolist, do: result, else: IO.iodata_to_binary(result) + {:ok, result, length + 5} end end @doc """ - Transform gRPC body to protobuf data + Transforms gRPC body into Protobuf data. ## Examples @@ -66,7 +87,7 @@ defmodule GRPC.Message do end @doc """ - Transform gRPC body to protobuf data with compressing + Transform gRPC body into Protobuf data with compression. ## Examples @@ -130,7 +151,7 @@ defmodule GRPC.Message do end @doc """ - Get message data from data buffer + Get message data from data buffer. ## Examples @@ -151,4 +172,28 @@ defmodule GRPC.Message do def get_message(_) do false end + + def get_message(data, nil = _compressor) do + case data do + <> -> + {{flag, message}, rest} + + _other -> + data + end + end + + def get_message(data, compressor) do + case data do + <<1::8, length::unsigned-integer-32, message::bytes-size(length), rest::binary>> -> + {{1, compressor.decompress(message)}, rest} + + <<0::8, length::unsigned-integer-32, message::bytes-size(length), rest::binary>> -> + {{0, message}, rest} + + _other -> + data + end + end end diff --git a/lib/grpc/message/protobuf.ex b/lib/grpc/message/protobuf.ex deleted file mode 100644 index 3a36537f9..000000000 --- a/lib/grpc/message/protobuf.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule GRPC.Message.Protobuf do - @moduledoc false - - # Module for encoding or decoding message using Protobuf. - - @spec encode(atom, struct) :: binary - def encode(mod, struct) do - apply(mod, :encode, [struct]) - end - - @spec decode(atom, binary) :: struct - def decode(mod, message) do - apply(mod, :decode, [message]) - end -end diff --git a/lib/grpc/protoc/cli.ex b/lib/grpc/protoc/cli.ex new file mode 100644 index 000000000..c60afae55 --- /dev/null +++ b/lib/grpc/protoc/cli.ex @@ -0,0 +1,226 @@ +defmodule GRPC.Protoc.CLI do + @moduledoc """ + `protoc` plugin for generating Elixir code. + + `protoc-gen-elixir` (this name is important) **must** be in `$PATH`. You are not supposed + to call it directly, but only through `protoc`. + + ## Examples + + $ protoc --elixir_out=./lib your.proto + $ protoc --elixir_out=plugins=grpc:./lib/ *.proto + $ protoc -I protos --elixir_out=./lib protos/namespace/*.proto + + Options: + + * --version Print version of protobuf-elixir + * --help (-h) Print this help + + """ + + alias Protobuf.Protoc.Context + + # Entrypoint for the escript (protoc-gen-elixir). + @doc false + @spec main([String.t()]) :: :ok + def main(args) + + def main(["--version"]) do + {:ok, version} = :application.get_key(:protobuf, :vsn) + IO.puts(version) + end + + def main([opt]) when opt in ["--help", "-h"] do + IO.puts(@moduledoc) + end + + # When called through protoc, all input is passed through stdin. + def main([] = _args) do + Protobuf.load_extensions() + + # See https://groups.google.com/forum/#!topic/elixir-lang-talk/T5enez_BBTI. + :io.setopts(:standard_io, encoding: :latin1) + + # Read the standard input that protoc feeds us. + bin = binread_all!(:stdio) + + request = Protobuf.Decoder.decode(bin, Google.Protobuf.Compiler.CodeGeneratorRequest) + + ctx = + %Context{} + |> parse_params(request.parameter || "") + |> find_types(request.proto_file, request.file_to_generate) + + files = + Enum.flat_map(request.file_to_generate, fn file -> + desc = Enum.find(request.proto_file, &(&1.name == file)) + GRPC.Protoc.Generator.generate(ctx, desc) + end) + + %Google.Protobuf.Compiler.CodeGeneratorResponse{ + file: files, + supported_features: supported_features() + } + |> Protobuf.encode_to_iodata() + |> IO.binwrite() + end + + def main(_args) do + raise "invalid arguments. See protoc-gen-elixir --help." + end + + def supported_features() do + # The only available feature is proto3 with optional fields. + # This is backwards compatible with proto2 optional fields. + Google.Protobuf.Compiler.CodeGeneratorResponse.Feature.value(:FEATURE_PROTO3_OPTIONAL) + end + + # Made public for testing. + @doc false + def parse_params(%Context{} = ctx, params_str) when is_binary(params_str) do + params_str + |> String.split(",") + |> Enum.reduce(ctx, &parse_param/2) + end + + defp parse_param("plugins=" <> plugins, ctx) do + %Context{ctx | plugins: String.split(plugins, "+")} + end + + defp parse_param("gen_descriptors=" <> value, ctx) do + case value do + "true" -> + %Context{ctx | gen_descriptors?: true} + + other -> + raise "invalid value for gen_descriptors option, expected \"true\", got: #{inspect(other)}" + end + end + + defp parse_param("package_prefix=" <> package, ctx) do + if package == "" do + raise "package_prefix can't be empty" + else + %Context{ctx | package_prefix: package} + end + end + + defp parse_param("transform_module=" <> module, ctx) do + %Context{ctx | transform_module: Module.concat([module])} + end + + defp parse_param("one_file_per_module=" <> value, ctx) do + case value do + "true" -> + %Context{ctx | one_file_per_module?: true} + + other -> + raise "invalid value for one_file_per_module option, expected \"true\", got: #{inspect(other)}" + end + end + + # defp parse_param("include_docs=" <> value, ctx) do + # case value do + # "true" -> + # %Context{ctx | include_docs?: true} + + # other -> + # raise "invalid value for include_docs option, expected \"true\", got: #{inspect(other)}" + # end + # end + + defp parse_param(_unknown, ctx) do + ctx + end + + # Made public for testing. + @doc false + @spec find_types(Context.t(), [Google.Protobuf.FileDescriptorProto.t()], [String.t()]) :: + Context.t() + def find_types(%Context{} = ctx, descs, files_to_generate) + when is_list(descs) and is_list(files_to_generate) do + global_type_mapping = + Map.new(descs, fn %Google.Protobuf.FileDescriptorProto{name: filename} = desc -> + {filename, find_types_in_proto(ctx, desc, files_to_generate)} + end) + + %Context{ctx | global_type_mapping: global_type_mapping} + end + + defp find_types_in_proto( + %Context{} = ctx, + %Google.Protobuf.FileDescriptorProto{} = desc, + files_to_generate + ) do + # Only take package_prefix into consideration for files that we're directly generating. + package_prefix = + if desc.name in files_to_generate do + ctx.package_prefix + else + nil + end + + ctx = + %Protobuf.Protoc.Context{ + namespace: [], + package_prefix: package_prefix, + package: desc.package + } + |> Protobuf.Protoc.Context.custom_file_options_from_file_desc(desc) + + find_types_in_descriptor(_types = %{}, ctx, desc.message_type ++ desc.enum_type) + end + + defp find_types_in_descriptor(types_acc, ctx, descs) when is_list(descs) do + Enum.reduce(descs, types_acc, &find_types_in_descriptor(_acc = &2, ctx, _desc = &1)) + end + + defp find_types_in_descriptor( + types_acc, + ctx, + %Google.Protobuf.DescriptorProto{name: name} = desc + ) do + new_ctx = update_in(ctx.namespace, &(&1 ++ [name])) + + types_acc + |> update_types(ctx, name) + |> find_types_in_descriptor(new_ctx, desc.enum_type) + |> find_types_in_descriptor(new_ctx, desc.nested_type) + end + + defp find_types_in_descriptor( + types_acc, + ctx, + %Google.Protobuf.EnumDescriptorProto{name: name} + ) do + update_types(types_acc, ctx, name) + end + + defp update_types(types, %Context{namespace: ns, package: pkg} = ctx, name) do + type_name = Protobuf.Protoc.Generator.Util.mod_name(ctx, ns ++ [name]) + + mapping_name = + ([pkg] ++ ns ++ [name]) + |> Enum.reject(&is_nil/1) + |> Enum.join(".") + + Map.put(types, "." <> mapping_name, %{type_name: type_name}) + end + + if Version.match?(System.version(), "~> 1.13") do + defp binread_all!(device) do + case IO.binread(device, :eof) do + data when is_binary(data) -> data + :eof -> _previous_behavior = "" + other -> raise "reading from #{inspect(device)} failed: #{inspect(other)}" + end + end + else + defp binread_all!(device) do + case IO.binread(device, :all) do + data when is_binary(data) -> data + other -> raise "reading from #{inspect(device)} failed: #{inspect(other)}" + end + end + end +end diff --git a/lib/grpc/protoc/generator.ex b/lib/grpc/protoc/generator.ex new file mode 100644 index 000000000..f7586885a --- /dev/null +++ b/lib/grpc/protoc/generator.ex @@ -0,0 +1,68 @@ +defmodule GRPC.Protoc.Generator do + @moduledoc false + + alias Protobuf.Protoc.Context + alias Protobuf.Protoc.Generator + + @spec generate(Context.t(), %Google.Protobuf.FileDescriptorProto{}) :: + [Google.Protobuf.Compiler.CodeGeneratorResponse.File.t()] + def generate(%Context{} = ctx, %Google.Protobuf.FileDescriptorProto{} = desc) do + module_definitions = + ctx + |> generate_module_definitions(desc) + |> Enum.reject(&is_nil/1) + + if ctx.one_file_per_module? do + Enum.map(module_definitions, fn {mod_name, content} -> + file_name = Macro.underscore(mod_name) <> ".svc.ex" + + %Google.Protobuf.Compiler.CodeGeneratorResponse.File{ + name: file_name, + content: content + } + end) + else + # desc.name is the filename, ending in ".proto". + file_name = Path.rootname(desc.name) <> ".svc.ex" + + content = + module_definitions + |> Enum.map(fn {_mod_name, contents} -> [contents, ?\n] end) + |> IO.iodata_to_binary() + |> Generator.Util.format() + + [ + %Google.Protobuf.Compiler.CodeGeneratorResponse.File{ + name: file_name, + content: content + } + ] + end + end + + defp generate_module_definitions(ctx, %Google.Protobuf.FileDescriptorProto{} = desc) do + ctx = + %Context{ + ctx + | syntax: syntax(desc.syntax), + package: desc.package, + dep_type_mapping: get_dep_type_mapping(ctx, desc.dependency, desc.name) + } + |> Protobuf.Protoc.Context.custom_file_options_from_file_desc(desc) + + Enum.map(desc.service, &GRPC.Protoc.Generator.Service.generate(ctx, &1)) + end + + defp get_dep_type_mapping(%Context{global_type_mapping: global_mapping}, deps, file_name) do + mapping = + Enum.reduce(deps, %{}, fn dep, acc -> + Map.merge(acc, global_mapping[dep]) + end) + + Map.merge(mapping, global_mapping[file_name]) + end + + defp syntax("proto3"), do: :proto3 + defp syntax("proto2"), do: :proto2 + defp syntax(nil), do: :proto2 +end diff --git a/lib/grpc/protoc/generator/service.ex b/lib/grpc/protoc/generator/service.ex new file mode 100644 index 000000000..107e302d4 --- /dev/null +++ b/lib/grpc/protoc/generator/service.ex @@ -0,0 +1,71 @@ +defmodule GRPC.Protoc.Generator.Service do + @moduledoc false + + alias Protobuf.Protoc.Context + alias Protobuf.Protoc.Generator.Util + + require EEx + + EEx.function_from_file( + :defp, + :service_template, + Path.expand("./templates/service.ex.eex", :code.priv_dir(:grpc)), + [:assigns] + ) + + @spec generate(Context.t(), Google.Protobuf.ServiceDescriptorProto.t()) :: + {String.t(), String.t()} + def generate(%Context{} = ctx, %Google.Protobuf.ServiceDescriptorProto{} = desc) do + # service can't be nested + mod_name = Util.mod_name(ctx, [Macro.camelize(desc.name)]) + name = Util.prepend_package_prefix(ctx.package, desc.name) + methods = Enum.map(desc.method, &generate_service_method(ctx, &1)) + + descriptor_fun_body = + if ctx.gen_descriptors? do + Util.descriptor_fun_body(desc) + else + nil + end + + {mod_name, + Util.format( + service_template( + module: mod_name, + service_name: name, + methods: methods, + descriptor_fun_body: descriptor_fun_body, + version: Util.version(), + module_doc?: false + ) + )} + end + + defp generate_service_method(ctx, method) do + input = service_arg(Util.type_from_type_name(ctx, method.input_type), method.client_streaming) + + output = + service_arg(Util.type_from_type_name(ctx, method.output_type), method.server_streaming) + + options = + method.options + |> opts() + |> inspect(limit: :infinity) + + {method.name, input, output, options} + end + + defp service_arg(type, _streaming? = true), do: "stream(#{type})" + defp service_arg(type, _streaming?), do: type + + defp opts(%Google.Protobuf.MethodOptions{__pb_extensions__: extensions}) + when extensions == %{} do + %{} + end + + defp opts(%Google.Protobuf.MethodOptions{__pb_extensions__: extensions}) do + for {{type, field}, value} <- extensions, into: %{} do + {field, %{type: type, value: value}} + end + end +end diff --git a/lib/grpc/rpc_error.ex b/lib/grpc/rpc_error.ex new file mode 100644 index 000000000..39ec37ed8 --- /dev/null +++ b/lib/grpc/rpc_error.ex @@ -0,0 +1,99 @@ +defmodule GRPC.RPCError do + @moduledoc """ + The RPC error raised in server side and got in client side. + + # server side + raise GRPC.RPCError, status: :unknown # preferred + raise GRPC.RPCError, status: GRPC.Status.unknown, message: "error message" + + # client side + {:error, error} = Your.Stub.unary_call(channel, request) + + Error handling can be done with the `is_rpc_error/2` guard. + + Expanding on the code above, the first option is for the guard to + be used in a `cond` or `case`, as follows: + + cond do + is_rpc_error(error, GRPC.Status.not_found()) -> + do_something_when_not_found() + + is_rpc_error(error, GRPC.Status.out_of_range()) -> + do_something_when_out_of_range() + + true -> + fallback_code() + end + + Another option is for the error handling to be written into a multi-clause function. + In such case we must define module attributes for each of the errors we want because + the functions in `GRPC.Status` can't be called directly inside the guard. + + ... + handle_error(error) + ... + + @not_found GRPC.Status.not_found() + @out_of_range GRPC.Status.out_of_range() + + defp handle_error(error) when is_rpc_error(error, @not_found) do + # not found + end + + defp handle_error(error) when is_rpc_error(error, @out_of_range) do + # out of range + end + + defp handle_error(error) do + # fallback + end + + See `GRPC.Status` for more details on possible statuses. + """ + + defexception [:status, :message] + + defguard is_rpc_error(e, status) when is_struct(e, __MODULE__) and e.status == status + + @type t :: %__MODULE__{status: GRPC.Status.t(), message: String.t()} + + alias GRPC.Status + + @spec new(status :: atom()) :: t() + def new(status) when is_atom(status) do + exception(status: status) + end + + @spec exception(args :: list()) :: t() + def exception(args) when is_list(args) do + error = parse_args(args, %__MODULE__{}) + + %{error | message: error.message || Status.status_message(error.status)} + end + + defp parse_args([], acc), do: acc + + defp parse_args([{:status, status} | t], acc) when is_integer(status) do + acc = %{acc | status: status} + parse_args(t, acc) + end + + defp parse_args([{:status, status} | t], acc) when is_atom(status) do + acc = %{acc | status: apply(GRPC.Status, status, [])} + parse_args(t, acc) + end + + defp parse_args([{:message, message} | t], acc) when is_binary(message) do + acc = %{acc | message: message} + parse_args(t, acc) + end + + @spec exception(status :: Status.t() | atom(), message :: String.t()) :: t() + def exception(status, message) when is_atom(status) do + %GRPC.RPCError{status: apply(GRPC.Status, status, []), message: message} + end + + def exception(status, message) when is_integer(status) do + %GRPC.RPCError{status: status, message: message} + end +end diff --git a/lib/grpc/server.ex b/lib/grpc/server.ex index 9274d954c..08f308e3d 100644 --- a/lib/grpc/server.ex +++ b/lib/grpc/server.ex @@ -29,30 +29,139 @@ defmodule GRPC.Server do The request will be a `Enumerable.t`(created by Elixir's `Stream`) of requests if it's streaming. If a reply is streaming, you need to call `send_reply/2` to send replies one by one instead of returning reply in the end. + + ## gRPC HTTP/JSON transcoding + + Transcoding can be enabled by using the option `http_transcode: true`: + + defmodule Greeter.Service do + use GRPC.Service, name: "ping" + + rpc :SayHello, Request, Reply + rpc :SayGoodbye, stream(Request), stream(Reply) + end + + defmodule Greeter.Server do + use GRPC.Server, service: Greeter.Service, http_transcode: true + + def say_hello(request, _stream) do + Reply.new(message: "Hello" <> request.name) + end + + def say_goodbye(request_enum, stream) do + requests = Enum.map request_enum, &(&1) + GRPC.Server.send_reply(stream, reply1) + GRPC.Server.send_reply(stream, reply2) + end + end + + With transcoding enabled gRPC methods can be used over HTTP/1 with JSON i.e + + POST localhost/helloworld.Greeter/SayHello` + Content-Type: application/json + { + "message": "gRPC" + } + + HTTP/1.1 200 OK + Content-Type: application/json + { + "message": "Hello gRPC" + } + + By using `option (google.api.http)` annotations in the `.proto` file the mapping between + HTTP/JSON to gRPC methods and parameters can be customized: + + syntax = "proto3"; + + import "google/api/annotations.proto"; + import "google/protobuf/timestamp.proto"; + + package helloworld; + + service Greeter { + rpc SayHello (HelloRequest) returns (HelloReply) { + option (google.api.http) = { + get: "/v1/greeter/{name}" + }; + } + } + + message HelloRequest { + string name = 1; + } + + message HelloReply { + string message = 1; + } + + In addition to the `POST localhost/helloworld.Greeter/SayHello` route in the previous examples + this creates an additional route: `GET localhost/v1/greeter/:name` + + GET localhost/v1/greeter/gRPC + Accept: application/json + + HTTP/1.1 200 OK + Content-Type: application/json + { + "message": "Hello gRPC" + } + + For more comprehensive documentation on annotation usage in `.proto` files [see](https://cloud.google.com/endpoints/docs/grpc/transcoding) """ require Logger - alias GRPC.Server.Stream alias GRPC.RPCError + alias GRPC.Server.Router + alias GRPC.Server.Transcode @type rpc_req :: struct | Enumerable.t() @type rpc_return :: struct | any - @type rpc :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return) + @type rpc :: (GRPC.Server.rpc_req(), GRPC.Server.Stream.t() -> rpc_return) defmacro __using__(opts) do quote bind_quoted: [opts: opts], location: :keep do + opts = + Keyword.validate!(opts, [ + :service, + codecs: [GRPC.Codec.Proto, GRPC.Codec.WebText, GRPC.Codec.JSON], + compressors: [], + http_transcode: false + ]) + service_mod = opts[:service] service_name = service_mod.__meta__(:name) - codecs = opts[:codecs] || [GRPC.Codec.Proto] - compressors = opts[:compressors] || [] + codecs = opts[:codecs] + compressors = opts[:compressors] + http_transcode = opts[:http_transcode] + + codecs = if http_transcode, do: [GRPC.Codec.JSON | codecs], else: codecs + + routes = + for {name, _, _, options} = rpc <- service_mod.__rpc_calls__(), reduce: [] do + acc -> + path = "/#{service_name}/#{name}" + + acc = + if http_transcode and Map.has_key?(options, :http) do + %{value: http_rule} = GRPC.Service.rpc_options(rpc, :http) + route = Macro.escape({:http_transcode, Router.build_route(http_rule)}) + [route | acc] + else + acc + end + + [{:grpc, path} | acc] + end - Enum.each(service_mod.__rpc_calls__(), fn {name, _, _} = rpc -> + Enum.each(service_mod.__rpc_calls__(), fn {name, _, _, options} = rpc -> func_name = name |> to_string |> Macro.underscore() |> String.to_atom() path = "/#{service_name}/#{name}" grpc_type = GRPC.Service.grpc_type(rpc) - def __call_rpc__(unquote(path), stream) do + def __call_rpc__(unquote(path), http_method, stream) + when http_method == :post or http_method == :options do GRPC.Server.call( unquote(service_mod), %{ @@ -65,34 +174,75 @@ defmodule GRPC.Server do unquote(func_name) ) end + + if http_transcode and Map.has_key?(options, :http) do + %{value: http_rule} = GRPC.Service.rpc_options(rpc, :http) + {http_method, http_path, _matches} = Router.build_route(http_rule) + + def __call_rpc__(unquote(http_path), unquote(http_method), stream) do + GRPC.Server.call( + unquote(service_mod), + %{ + stream + | service_name: unquote(service_name), + method_name: unquote(to_string(name)), + grpc_type: unquote(grpc_type), + http_method: unquote(http_method) + }, + unquote(Macro.escape(put_elem(rpc, 0, func_name))), + unquote(func_name) + ) + end + end end) - def __call_rpc__(_, stream) do + def __call_rpc__(_http_path, _http_method, stream) do raise GRPC.RPCError, status: :unimplemented end + def service_name(_) do + "" + end + def __meta__(:service), do: unquote(service_mod) def __meta__(:codecs), do: unquote(codecs) def __meta__(:compressors), do: unquote(compressors) + def __meta__(:routes), do: unquote(routes) end end - @type servers_map :: %{String.t() => [module]} - @type servers_list :: module | [module] - @doc false - @spec call(atom, Stream.t(), tuple, atom) :: {:ok, Stream.t(), struct} | {:ok, struct} + @spec call(atom(), GRPC.Server.Stream.t(), tuple(), atom()) :: + {:ok, GRPC.Server.Stream.t(), struct()} | {:ok, struct()} def call( _service_mod, stream, - {_, {req_mod, req_stream}, {res_mod, res_stream}} = rpc, + {_, {req_mod, req_stream}, {res_mod, res_stream}, _options} = rpc, func_name ) do - stream = %{stream | request_mod: req_mod, response_mod: res_mod, rpc: rpc} + request_id = generate_request_id() + + stream = %{ + stream + | request_mod: req_mod, + request_id: request_id, + response_mod: res_mod, + rpc: rpc + } handle_request(req_stream, res_stream, stream, func_name) end + defp generate_request_id do + binary = << + System.system_time(:nanosecond)::64, + :erlang.phash2({node(), self()}, 16_777_216)::24, + :erlang.unique_integer()::32 + >> + + Base.url_encode64(binary, padding: false) + end + defp handle_request(req_s, res_s, %{server: server} = stream, func_name) do if function_exported?(server, func_name, 2) do do_handle_request(req_s, res_s, stream, func_name) @@ -101,6 +251,38 @@ defmodule GRPC.Server do end end + defp do_handle_request( + false, + res_stream, + %{ + rpc: rpc, + request_mod: req_mod, + codec: codec, + adapter: adapter, + payload: payload, + access_mode: :http_transcoding + } = stream, + func_name + ) do + {:ok, data} = adapter.read_body(payload) + request_body = codec.decode(data, req_mod) + rule = GRPC.Service.rpc_options(rpc, :http) || %{value: %{}} + bindings = adapter.get_bindings(payload) + qs = adapter.get_qs(payload) + + case Transcode.map_request(rule.value, request_body, bindings, qs, req_mod) do + {:ok, request} -> + call_with_interceptors(res_stream, func_name, stream, request) + + resp = {:error, _} -> + resp + end + end + + defp do_handle_request(false, res_stream, %{is_preflight?: true} = stream, func_name) do + call_with_interceptors(res_stream, func_name, stream, []) + end + defp do_handle_request( false, res_stream, @@ -109,7 +291,14 @@ defmodule GRPC.Server do ) do {:ok, data} = adapter.read_body(payload) - case GRPC.Message.from_data(stream, data) do + body = + if function_exported?(codec, :unpack_from_channel, 1) do + codec.unpack_from_channel(data) + else + data + end + + case GRPC.Message.from_data(stream, body) do {:ok, message} -> request = codec.decode(message, req_mod) @@ -160,35 +349,27 @@ defmodule GRPC.Server do %{server: server, endpoint: endpoint} = stream, req ) do - last = fn r, s -> - reply = apply(server, func_name, [r, s]) - - if res_stream do - {:ok, stream} - else - {:ok, stream, reply} + GRPC.Telemetry.server_span(server, endpoint, func_name, stream, fn -> + last = fn r, s -> + # no response is rquired for preflight requests + reply = if stream.is_preflight?, do: [], else: apply(server, func_name, [r, s]) + + if res_stream do + {:ok, stream} + else + {:ok, stream, reply} + end end - end - interceptors = interceptors(endpoint, server) + interceptors = interceptors(endpoint, server) - next = - Enum.reduce(interceptors, last, fn {interceptor, opts}, acc -> - fn r, s -> interceptor.call(r, s, acc, opts) end - end) + next = + Enum.reduce(interceptors, last, fn {interceptor, opts}, acc -> + fn r, s -> interceptor.call(r, s, acc, opts) end + end) - try do next.(req, stream) - rescue - e in GRPC.RPCError -> - {:error, e} - catch - kind, reason -> - stack = __STACKTRACE__ - Logger.error(Exception.format(kind, reason, stack)) - reason = Exception.normalize(kind, reason, stack) - {:error, %{kind: kind, reason: reason, stack: stack}} - end + end) end defp interceptors(nil, _), do: [] @@ -214,21 +395,27 @@ defmodule GRPC.Server do # # * `:cred` - a credential created by functions of `GRPC.Credential`, # an insecure server will be created without this option - # * `:adapter` - use a custom server adapter instead of default `GRPC.Adapter.Cowboy` + # * `:adapter` - use a custom server adapter instead of default `GRPC.Server.Adapters.Cowboy` + # * `:adapter_opts` - configuration for the specified adapter. + # * `:status_handler` - adds a status handler that could be listening on HTTP/1, if necessary. + # It should follow the format defined by cowboy_router:compile/3 @doc false - @spec start(servers_list, non_neg_integer, Keyword.t()) :: {atom, any, non_neg_integer} + @spec start(module() | [module()], non_neg_integer(), Keyword.t()) :: + {atom(), any(), non_neg_integer()} | {:error, any()} def start(servers, port, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + adapter = Keyword.get(opts, :adapter) || GRPC.Server.Adapters.Cowboy servers = GRPC.Server.servers_to_map(servers) adapter.start(nil, servers, port, opts) end @doc false - @spec start_endpoint(atom, non_neg_integer, Keyword.t()) :: {atom, any, non_neg_integer} + @spec start_endpoint(atom(), non_neg_integer(), Keyword.t()) :: + {atom(), any(), non_neg_integer()} def start_endpoint(endpoint, port, opts \\ []) do + opts = Keyword.validate!(opts, adapter: GRPC.Server.Adapters.Cowboy) + adapter = opts[:adapter] servers = endpoint.__meta__(:servers) servers = GRPC.Server.servers_to_map(servers) - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) adapter.start(endpoint, servers, port, opts) end @@ -240,32 +427,26 @@ defmodule GRPC.Server do # # ## Options # - # * `:adapter` - use a custom adapter instead of default `GRPC.Adapter.Cowboy` + # * `:adapter` - use a custom adapter instead of default `GRPC.Server.Adapters.Cowboy` @doc false - @spec stop(servers_list, Keyword.t()) :: any + @spec stop(module() | [module()], Keyword.t()) :: any() def stop(servers, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + opts = Keyword.validate!(opts, adapter: GRPC.Server.Adapters.Cowboy) + adapter = opts[:adapter] servers = GRPC.Server.servers_to_map(servers) adapter.stop(nil, servers) end @doc false - @spec stop_endpoint(atom, Keyword.t()) :: any + @spec stop_endpoint(atom(), Keyword.t()) :: any() def stop_endpoint(endpoint, opts \\ []) do - adapter = Keyword.get(opts, :adapter, GRPC.Adapter.Cowboy) + opts = Keyword.validate!(opts, adapter: GRPC.Server.Adapters.Cowboy) + adapter = opts[:adapter] servers = endpoint.__meta__(:servers) servers = GRPC.Server.servers_to_map(servers) adapter.stop(endpoint, servers) end - @doc """ - DEPRECATED. Use `send_reply/3` instead - """ - @deprecated "Use send_reply/3 instead" - def stream_send(stream, reply) do - send_reply(stream, reply) - end - @doc """ Send streaming reply. @@ -273,8 +454,12 @@ defmodule GRPC.Server do iex> GRPC.Server.send_reply(stream, reply) """ - @spec send_reply(Stream.t(), struct) :: Stream.t() - def send_reply(%{__interface__: interface} = stream, reply, opts \\ []) do + @spec send_reply(GRPC.Server.Stream.t(), struct()) :: GRPC.Server.Stream.t() + def send_reply( + %{__interface__: interface} = stream, + reply, + opts \\ [] + ) do interface[:send_reply].(stream, reply, opts) end @@ -283,7 +468,7 @@ defmodule GRPC.Server do You can send headers only once, before that you can set headers using `set_headers/2`. """ - @spec send_headers(Stream.t(), map) :: Stream.t() + @spec send_headers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def send_headers(%{adapter: adapter} = stream, headers) do adapter.send_headers(stream.payload, headers) stream @@ -294,7 +479,7 @@ defmodule GRPC.Server do You can set headers more than once. """ - @spec set_headers(Stream.t(), map) :: Stream.t() + @spec set_headers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def set_headers(%{adapter: adapter} = stream, headers) do adapter.set_headers(stream.payload, headers) stream @@ -303,7 +488,7 @@ defmodule GRPC.Server do @doc """ Set custom trailers, which will be sent in the end. """ - @spec set_trailers(Stream.t(), map) :: Stream.t() + @spec set_trailers(GRPC.Server.Stream.t(), map()) :: GRPC.Server.Stream.t() def set_trailers(%{adapter: adapter} = stream, trailers) do adapter.set_resp_trailers(stream.payload, trailers) stream @@ -313,7 +498,7 @@ defmodule GRPC.Server do Set compressor to compress responses. An accepted compressor will be set if clients use one, even if `set_compressor` is not called. But this can be called to override the chosen. """ - @spec set_compressor(Stream.t(), module) :: Stream.t() + @spec set_compressor(GRPC.Server.Stream.t(), module()) :: GRPC.Server.Stream.t() def set_compressor(%{adapter: adapter} = stream, compressor) do adapter.set_compressor(stream.payload, compressor) stream @@ -326,14 +511,7 @@ defmodule GRPC.Server do end @doc false - @spec service_name(String.t()) :: String.t() - def service_name(path) do - ["", name | _] = String.split(path, "/") - name - end - - @doc false - @spec servers_to_map(servers_list) :: servers_map + @spec servers_to_map(module() | [module()]) :: %{String.t() => [module()]} def servers_to_map(servers) do Enum.reduce(List.wrap(servers), %{}, fn s, acc -> Map.put(acc, s.__meta__(:service).__meta__(:name), s) diff --git a/lib/grpc/server/adapter.ex b/lib/grpc/server/adapter.ex new file mode 100644 index 000000000..9884ac9ff --- /dev/null +++ b/lib/grpc/server/adapter.ex @@ -0,0 +1,27 @@ +defmodule GRPC.Server.Adapter do + @moduledoc """ + HTTP server adapter for GRPC. + """ + + @type state :: %{ + pid: pid, + handling_timer: reference | nil, + resp_trailers: map, + compressor: atom | nil, + pending_reader: nil + } + + @callback start( + atom(), + %{String.t() => [module()]}, + port :: non_neg_integer(), + opts :: keyword() + ) :: + {atom(), any(), non_neg_integer()} + + @callback stop(atom(), %{String.t() => [module()]}) :: :ok | {:error, :not_found} + + @callback send_reply(state, content :: binary(), opts :: keyword()) :: any() + + @callback send_headers(state, headers :: map()) :: any() +end diff --git a/lib/grpc/adapter/cowboy.ex b/lib/grpc/server/adapters/cowboy.ex similarity index 52% rename from lib/grpc/adapter/cowboy.ex rename to lib/grpc/server/adapters/cowboy.ex index 31bc09941..3395c4a85 100644 --- a/lib/grpc/adapter/cowboy.ex +++ b/lib/grpc/server/adapters/cowboy.ex @@ -1,21 +1,34 @@ -defmodule GRPC.Adapter.Cowboy do - @moduledoc false +defmodule GRPC.Server.Adapters.Cowboy do + @moduledoc """ + A server (`b:GRPC.Server.Adapter`) adapter using `:cowboy`. - # A server(`GRPC.Server`) adapter using Cowboy. - # Cowboy req will be stored in `:payload` of `GRPC.Server.Stream`. + Cowboy requests will be stored in the `:payload` field of the `GRPC.Server.Stream`. + """ - # Waiting for this is released on hex https://github.com/ninenines/ranch/pull/227 - @dialyzer {:nowarn_function, running_info: 4} + @behaviour GRPC.Server.Adapter require Logger - alias GRPC.Adapter.Cowboy.Handler, as: Handler + alias GRPC.Server.Adapters.Cowboy.Handler @default_num_acceptors 20 @default_max_connections 16384 - # Only used in starting a server manually using `GRPC.Server.start(servers)` - @spec start(atom, GRPC.Server.servers_map(), non_neg_integer, keyword) :: - {:ok, pid, non_neg_integer} + @doc """ + Starts a Cowboy server. Only used in starting a server manually using `GRPC.Server.start(servers)`. Otherwise `GRPC.Server.Adapters.Cowboy.child_spec/4` is used. + + The available options below are a subset of [`ranch_tcp`](https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_tcp/)'s options. + + ## Options + * `:net` - If using `:inet` (IPv4 only - the default) or `:inet6` (IPv6) + * `:ip` - The IP to bind the server to. + Must be either a tuple in the format `{a, b, c, d}` with each value in `0..255` for IPv4, + or a tuple in the format `{a, b, c, d, e, f, g, h}` with each value in `0..65535` for IPv6, + or a tuple in the format `{:local, path}` for a unix socket at the given `path`. + If both `:net` and `:ip` options are given, make sure they are compatible + (i.e. give a IPv4 for `:inet` and IPv6 for `:inet6`). The default is to listen on all interfaces. + * `:ipv6_v6only` - If true, only bind on IPv6 addresses (default: `false`). + """ + @impl true def start(endpoint, servers, port, opts) do start_args = cowboy_start_args(endpoint, servers, port, opts) start_func = if opts[:cred], do: :start_tls, else: :start_clear @@ -30,8 +43,11 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec child_spec(atom, GRPC.Server.servers_map(), non_neg_integer, Keyword.t()) :: - Supervisor.Spec.spec() + @doc """ + Return a child_spec to start server. See `GRPC.Server.Adapters.Cowboy.start/4` for a list of supported options. + """ + @spec child_spec(atom(), %{String.t() => [module()]}, non_neg_integer(), Keyword.t()) :: + Supervisor.child_spec() def child_spec(endpoint, servers, port, opts) do [ref, trans_opts, proto_opts] = cowboy_start_args(endpoint, servers, port, opts) trans_opts = Map.put(trans_opts, :connection_type, :supervisor) @@ -43,17 +59,31 @@ defmodule GRPC.Adapter.Cowboy do {:ranch_tcp, :cowboy_clear} end - {ref, mfa, type, timeout, kind, modules} = - :ranch.child_spec(ref, transport, trans_opts, protocol, proto_opts) + # Ideally, we would just update Ranch, but compatibility issues with cowboy hold us back on this + # So we just support both child spec versions here instead + case :ranch.child_spec(ref, transport, trans_opts, protocol, proto_opts) do + {ref, mfa, type, timeout, kind, modules} -> + scheme = if opts[:cred], do: :https, else: :http + # Wrap real mfa to print starting log + wrapped_mfa = {__MODULE__, :start_link, [scheme, endpoint, servers, mfa]} - scheme = if opts[:cred], do: :https, else: :http - # Wrap real mfa to print starting log - mfa = {__MODULE__, :start_link, [scheme, endpoint, servers, mfa]} - {ref, mfa, type, timeout, kind, modules} + %{ + id: ref, + start: wrapped_mfa, + restart: type, + shutdown: timeout, + type: kind, + modules: modules + } + + child_spec when is_map(child_spec) -> + child_spec + end end # spec: :supervisor.mfargs doesn't work - @spec start_link(atom, atom, GRPC.Server.servers_map(), any) :: {:ok, pid} | {:error, any} + @spec start_link(atom(), atom(), %{String.t() => [module()]}, any()) :: + {:ok, pid()} | {:error, any()} def start_link(scheme, endpoint, servers, {m, f, [ref | _] = a}) do case apply(m, f, a) do {:ok, pid} -> @@ -73,17 +103,17 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec stop(atom, GRPC.Server.servers_map()) :: :ok | {:error, :not_found} + @impl true def stop(endpoint, servers) do :cowboy.stop_listener(servers_name(endpoint, servers)) end - @spec read_body(GRPC.Adapter.Cowboy.Handler.state()) :: {:ok, binary} + @spec read_body(GRPC.Server.Adapter.state()) :: {:ok, binary()} def read_body(%{pid: pid}) do Handler.read_full_body(pid) end - @spec reading_stream(GRPC.Adapter.Cowboy.Handler.state()) :: Enumerable.t() + @spec reading_stream(GRPC.Server.Adapter.state()) :: Enumerable.t() def reading_stream(%{pid: pid}) do Stream.unfold(%{pid: pid, need_more: true, buffer: <<>>}, fn acc -> read_stream(acc) end) end @@ -115,11 +145,13 @@ defmodule GRPC.Adapter.Cowboy do end end - @spec send_reply(GRPC.Adapter.Cowboy.Handler.state(), binary, keyword) :: any + @impl true def send_reply(%{pid: pid}, data, opts) do - Handler.stream_body(pid, data, opts, :nofin) + http_transcode = Keyword.get(opts, :http_transcode) + Handler.stream_body(pid, data, opts, :nofin, http_transcode) end + @impl true def send_headers(%{pid: pid}, headers) do Handler.stream_reply(pid, 200, headers) end @@ -148,25 +180,61 @@ defmodule GRPC.Adapter.Cowboy do Handler.get_cert(pid) end + def get_qs(%{pid: pid}) do + Handler.get_qs(pid) + end + + def get_bindings(%{pid: pid}) do + Handler.get_bindings(pid) + end + def set_compressor(%{pid: pid}, compressor) do Handler.set_compressor(pid, compressor) end + defp build_handlers(endpoint, servers, opts) do + Enum.flat_map(servers, fn {_name, server_mod} = server -> + routes = server_mod.__meta__(:routes) + Enum.map(routes, &build_route(&1, endpoint, server, opts)) + end) + end + + defp build_route({:grpc, path}, endpoint, server, opts) do + {path, GRPC.Server.Adapters.Cowboy.Handler, {endpoint, server, path, Enum.into(opts, %{})}} + end + + defp build_route({:http_transcode, {_method, path, match}}, endpoint, server, opts) do + {match, GRPC.Server.Adapters.Cowboy.Handler, {endpoint, server, path, Enum.into(opts, %{})}} + end + defp cowboy_start_args(endpoint, servers, port, opts) do - dispatch = - :cowboy_router.compile([ - {:_, [{:_, GRPC.Adapter.Cowboy.Handler, {endpoint, servers, Enum.into(opts, %{})}}]} - ]) + # Custom handler to be able to listen in the same port, more info: + # https://github.com/containous/traefik/issues/6211 + {adapter_opts, opts} = Keyword.pop(opts, :adapter_opts, []) + status_handler = Keyword.get(adapter_opts, :status_handler) - idle_timeout = Keyword.get(opts, :idle_timeout, :infinity) - num_acceptors = Keyword.get(opts, :num_acceptors, @default_num_acceptors) - max_connections = Keyword.get(opts, :max_connections, @default_max_connections) + handlers = build_handlers(endpoint, servers, opts) + + handlers = + if status_handler do + [status_handler | handlers] + else + handlers + end + + dispatch = GRPC.Server.Adapters.Cowboy.Router.compile([{:_, handlers}]) + dispatch_key = Module.concat(endpoint, Dispatch) + :persistent_term.put(dispatch_key, dispatch) + + idle_timeout = Keyword.get(opts, :idle_timeout) || :infinity + num_acceptors = Keyword.get(opts, :num_acceptors) || @default_num_acceptors + max_connections = Keyword.get(opts, :max_connections) || @default_max_connections # https://ninenines.eu/docs/en/cowboy/2.7/manual/cowboy_http2/ opts = Map.merge( %{ - env: %{dispatch: dispatch}, + env: %{dispatch: {:persistent_term, dispatch_key}}, idle_timeout: idle_timeout, inactivity_timeout: idle_timeout, settings_timeout: idle_timeout, @@ -193,7 +261,14 @@ defmodule GRPC.Adapter.Cowboy do defp socket_opts(port, opts) do socket_opts = [port: port] - socket_opts = if opts[:ip], do: [{:ip, opts[:ip]} | socket_opts], else: socket_opts + + # https://ninenines.eu/docs/en/ranch/1.7/manual/ranch_tcp/ + socket_opts = + Enum.reduce(opts, socket_opts, fn + {k, v}, acc when k in [:ip, :ipv6_v6only] and not is_nil(v) -> [{k, v} | acc] + {:net, v}, acc when not is_nil(v) -> [v | acc] + _, acc -> acc + end) if opts[:cred] do opts[:cred].ssl ++ @@ -215,8 +290,8 @@ defmodule GRPC.Adapter.Cowboy do addr_str = case addr do - :local -> - port + :undefined -> + raise "undefined address for ranch server" addr -> "#{:inet.ntoa(addr)}:#{port}" diff --git a/lib/grpc/server/adapters/cowboy/handler.ex b/lib/grpc/server/adapters/cowboy/handler.ex new file mode 100644 index 000000000..13e9e3f7a --- /dev/null +++ b/lib/grpc/server/adapters/cowboy/handler.ex @@ -0,0 +1,687 @@ +defmodule GRPC.Server.Adapters.Cowboy.Handler do + @moduledoc """ + A cowboy handler accepting all requests and calls corresponding functions defined by users. + """ + + alias GRPC.Server.Adapters.ReportException + alias GRPC.Transport.HTTP2 + alias GRPC.RPCError + require Logger + + @behaviour :cowboy_loop + + @adapter GRPC.Server.Adapters.Cowboy + @default_trailers HTTP2.server_trailers() + + @type init_state :: { + endpoint :: atom(), + server :: {name :: String.t(), module()}, + route :: String.t(), + opts :: keyword() + } + + @type pending_reader :: { + cowboy_read_ref :: reference, + server_rpc_pid :: pid, + server_rpc_reader_reference :: reference + } + @type stream_state :: %{ + pid: server_rpc_pid :: pid, + handling_timer: timeout_timer_ref :: reference, + pending_reader: nil | pending_reader, + access_mode: GRPC.Server.Stream.access_mode() + } + @type init_result :: + {:cowboy_loop, :cowboy_req.req(), stream_state} | {:ok, :cowboy_req.req(), init_state} + + @type is_fin :: :fin | :nofin + + @type stream_body_opts :: {:code, module()} | {:compress, boolean()} + + @type headers :: %{binary() => binary()} + + @doc """ + This function is meant to be called whenever a new request arrives to an existing connection. + This handler works mainly with two linked processes. + One of them is the process started by cowboy which internally we'll refer to it as `stream_pid`, + this process is responsible to interface the interactions with the open socket. + The second process is the one we start in this function, we'll refer to it as `server_rpc_pid`, + which is the point where we call the functions implemented by users (aka the modules who use + the `GRPC.Server` macro) + """ + @spec init(:cowboy_req.req(), state :: init_state) :: init_result + def init(req, {endpoint, {_name, server}, route, opts} = state) do + http_method = + req + |> :cowboy_req.method() + |> String.downcase() + |> String.to_existing_atom() + + with {:ok, access_mode, sub_type, content_type} <- find_content_type_subtype(req), + {:ok, codec} <- find_codec(sub_type, content_type, server), + {:ok, compressor} <- find_compressor(req, server) do + stream_pid = self() + http_transcode = access_mode == :http_transcoding + request_headers = :cowboy_req.headers(req) + + stream = %GRPC.Server.Stream{ + server: server, + endpoint: endpoint, + adapter: @adapter, + payload: %{pid: stream_pid}, + local: opts[:local], + codec: codec, + http_method: http_method, + http_request_headers: request_headers, + http_transcode: http_transcode, + compressor: compressor, + is_preflight?: preflight?(req), + access_mode: access_mode + } + + server_rpc_pid = :proc_lib.spawn_link(__MODULE__, :call_rpc, [server, route, stream]) + Process.flag(:trap_exit, true) + + req = :cowboy_req.set_resp_headers(HTTP2.server_headers(stream), req) + + timeout = Map.get(request_headers, "grpc-timeout") + + timer_ref = + if is_binary(timeout) do + Process.send_after( + self(), + {:handling_timeout, self()}, + GRPC.Transport.Utils.decode_timeout(timeout) + ) + end + + { + :cowboy_loop, + req, + %{ + pid: server_rpc_pid, + handling_timer: timer_ref, + pending_reader: nil, + access_mode: access_mode + } + } + else + {:error, error} -> + Logger.error(fn -> inspect(error) end) + trailers = HTTP2.server_trailers(error.status, error.message) + req = send_error_trailers(req, 200, trailers) + {:ok, req, state} + end + end + + defp find_codec(subtype, content_type, server) do + if codec = Enum.find(server.__meta__(:codecs), nil, fn c -> c.name() == subtype end) do + {:ok, codec} + else + {:error, + RPCError.exception( + status: :unimplemented, + message: "No codec registered for content-type #{content_type}" + )} + end + end + + defp find_content_type_subtype(req) do + content_type = + case :cowboy_req.header("content-type", req) do + :undefined -> + :cowboy_req.header("accept", req) + + content_type -> + content_type + end + + {:ok, access_mode, subtype} = extract_subtype(content_type) + access_mode = resolve_access_mode(req, access_mode, subtype) + {:ok, access_mode, subtype, content_type} + end + + defp find_compressor(req, server) do + encoding = :cowboy_req.header("grpc-encoding", req) + + if is_binary(encoding) do + compressor = Enum.find(server.__meta__(:compressors), nil, fn c -> c.name() == encoding end) + + if compressor do + {:ok, compressor} + else + {:error, + RPCError.exception( + status: :unimplemented, + message: "Not found compressor registered for grpc-encoding #{encoding}" + )} + end + else + {:ok, nil} + end + end + + # APIs begin + @doc """ + Synchronously reads the whole body content of a given request. + Raise in case of a timeout. + """ + @spec read_full_body(stream_pid :: pid) :: binary() + def read_full_body(pid) do + sync_call(pid, :read_full_body) + end + + @doc """ + Synchronously reads a chunk of body content of a given request. + Raise in case of a timeout. + """ + @spec read_body(stream_pid :: pid) :: binary() + def read_body(pid) do + sync_call(pid, :read_body) + end + + @doc """ + Asynchronously send back to client a chunk of `data`, when `http_transcode?` is true, the + data is sent back as it's, with no transformation of protobuf binaries to http2 data frames. + """ + @spec stream_body( + stream_pid :: pid, + data :: iodata, + opts :: list(stream_body_opts), + is_fin, + http_transcode? :: boolean() + ) :: :ok + def stream_body(pid, data, opts, is_fin, http_transcode? \\ false) do + send(pid, {:stream_body, data, opts, is_fin, http_transcode?}) + :ok + end + + @doc """ + Asynchronously send back to the client the http status and the headers for a given request. + """ + @spec stream_reply(stream_pid :: pid, status :: non_neg_integer(), headers :: headers) :: :ok + def stream_reply(pid, status, headers) do + send(pid, {:stream_reply, status, headers}) + :ok + end + + @doc """ + Asynchronously set the headers for a given request. This function does not send any + data back to the client. It simply appends the headers to be used in the response. + """ + @spec set_resp_headers(stream_pid :: pid, headers :: headers) :: :ok + def set_resp_headers(pid, headers) do + send(pid, {:set_resp_headers, headers}) + :ok + end + + @doc """ + Asynchronously set the trailer headers for a given request. This function does not send any + data back to the client. It simply appends the trailer headers to be used in the response. + """ + @spec set_resp_trailers(stream_pid :: pid, trailers :: headers) :: :ok + def set_resp_trailers(pid, trailers) do + send(pid, {:set_resp_trailers, trailers}) + :ok + end + + @doc """ + Asynchronously set the compressor algorithm to be used for compress the responses. This checks if + the `grpc-accept-encoding` header is present on the original request, otherwise no compression + is applied. + """ + @spec set_compressor(stream_pid :: pid, compressor :: module) :: :ok + def set_compressor(pid, compressor) do + send(pid, {:set_compressor, compressor}) + :ok + end + + @doc """ + Asynchronously stream the given trailers of request back to client. + """ + @spec stream_trailers(stream_pid :: pid, trailers :: headers) :: :ok + def stream_trailers(pid, trailers) do + send(pid, {:stream_trailers, trailers}) + :ok + end + + @doc """ + Return all request headers. + """ + @spec get_headers(stream_pid :: pid) :: :cowboy.http_headers() + def get_headers(pid) do + sync_call(pid, :get_headers) + end + + @doc """ + Return the peer IP address and port number + """ + @spec get_peer(stream_pid :: pid) :: {:inet.ip_address(), :inet.port_number()} + def get_peer(pid) do + sync_call(pid, :get_peer) + end + + @doc """ + Return the client TLS certificate. `:undefined` is returned if no certificate was specified + when establishing the connection. + """ + @spec get_cert(stream_pid :: pid) :: binary() | :undefined + def get_cert(pid) do + sync_call(pid, :get_cert) + end + + @doc """ + Return the query string for the request URI. + """ + @spec get_qs(stream_pid :: pid) :: binary() + def get_qs(pid) do + sync_call(pid, :get_qs) + end + + @doc """ + Return all bindings of a given request. + """ + @spec get_bindings(stream_pid :: pid) :: :cowboy_router.bindings() + def get_bindings(pid) do + sync_call(pid, :get_bindings) + end + + defp sync_call(pid, key) do + ref = make_ref() + send(pid, {key, ref, self()}) + + receive do + {^ref, msg} -> msg + end + end + + # APIs end + + def info({:read_full_body, ref, pid}, req, state) do + {s, body, req} = read_full_body(req, "", state[:handling_timer]) + send(pid, {ref, {s, body}}) + {:ok, req, state} + catch + :exit, :timeout -> + Logger.warning("Timeout when reading full body") + info({:handling_timeout, self()}, req, state) + end + + def info({:read_body, ref, pid}, req, state) do + opts = timeout_left_opt(state[:handling_timer]) + + case async_read_body(req, opts) do + {:send, {s, body, req}} -> + send(pid, {ref, {s, body}}) + {:ok, req, state} + + {:wait, read_ref} -> + {:ok, req, %{state | pending_reader: {read_ref, pid, ref}}} + end + end + + def info({:request_body, ref, :nofin, body}, req, %{pending_reader: {ref, pid, reader_ref}} = s) do + send(pid, {reader_ref, {:more, body}}) + + {:ok, req, %{s | pending_reader: nil}} + end + + def info( + {:request_body, ref, :fin, body_length, body}, + %{headers: headers} = req, + %{pending_reader: {ref, pid, reader_ref}} = s + ) do + send(pid, {reader_ref, {:ok, body}}) + + # cowboy_req's set_body_length + req = + Map.merge(req, %{ + headers: Map.put(headers, "content-length", Integer.to_string(body_length)), + body_length: body_length, + has_read_body: true + }) + + {:ok, req, s} + end + + def info({:get_headers, ref, pid}, req, state) do + headers = :cowboy_req.headers(req) + send(pid, {ref, headers}) + {:ok, req, state} + end + + def info({:get_peer, ref, pid}, req, state) do + peer = :cowboy_req.peer(req) + send(pid, {ref, peer}) + {:ok, req, state} + end + + def info({:get_cert, ref, pid}, req, state) do + peer = :cowboy_req.cert(req) + send(pid, {ref, peer}) + {:ok, req, state} + end + + def info({:get_qs, ref, pid}, req, state) do + qs = :cowboy_req.qs(req) + send(pid, {ref, qs}) + {:ok, req, state} + end + + def info({:get_bindings, ref, pid}, req, state) do + bindings = :cowboy_req.bindings(req) + send(pid, {ref, bindings}) + {:ok, req, state} + end + + # Handle http/json transcoded response + def info({:stream_body, data, _opts, is_fin, _http_transcode = true}, req, state) do + # TODO Compress + req = check_sent_resp(req) + :cowboy_req.stream_body(data, is_fin, req) + {:ok, req, state} + end + + def info({:stream_body, data, opts, is_fin, _}, req, state) do + # If compressor exists, compress is true by default + compressor = + if opts[:compress] == false do + nil + else + state[:compressor] + end + + accepted_encodings = + case :cowboy_req.header("grpc-accept-encoding", req) do + s when is_binary(s) -> + String.split(s, ",") + + _ -> + [] + end + + if compressor && !Enum.member?(accepted_encodings, compressor.name()) do + msg = + "A unaccepted encoding #{compressor.name()} is set, valid are: #{:cowboy_req.header("grpc-accept-encoding", req)}" + + error = RPCError.exception(status: :internal, message: msg) + req = send_error(req, error, state, :rpc_error) + + {:stop, req, state} + else + case GRPC.Message.to_data(data, compressor: compressor, codec: opts[:codec]) do + {:ok, data, _size} -> + req = check_sent_resp(req) + :cowboy_req.stream_body(data, is_fin, req) + {:ok, req, state} + + {:error, msg} -> + error = RPCError.exception(status: :internal, message: msg) + req = send_error(req, error, state, :rpc_error) + {:stop, req, state} + end + end + end + + def info({:stream_reply, status, headers}, req, state) do + req = :cowboy_req.stream_reply(status, headers, req) + {:ok, req, state} + end + + def info({:set_resp_headers, headers}, req, state) do + req = :cowboy_req.set_resp_headers(headers, req) + {:ok, req, state} + end + + def info({:set_resp_trailers, trailers}, req, state) do + {:ok, req, Map.put(state, :resp_trailers, trailers)} + end + + def info({:stream_trailers, trailers}, req, state) do + metadata = Map.get(state, :resp_trailers, %{}) + metadata = GRPC.Transport.HTTP2.encode_metadata(metadata) + send_stream_trailers(req, Map.merge(metadata, trailers)) + {:ok, req, state} + end + + def info({:handling_timeout, _}, req, state) do + error = %RPCError{status: GRPC.Status.deadline_exceeded(), message: "Deadline expired"} + req = send_error(req, error, state, :timeout) + + {:stop, req, state} + end + + def info({:set_compressor, compressor}, req, state) do + accept_encoding = :cowboy_req.header("grpc-accept-encoding", req) + + if is_binary(accept_encoding) do + req = :cowboy_req.set_resp_headers(%{"grpc-encoding" => compressor.name()}, req) + {:ok, req, Map.put(state, :compressor, compressor)} + else + {:ok, req, state} + end + end + + def info({:EXIT, server_rpc_pid, reason}, req, state = %{pid: server_rpc_pid}) + when reason in [:normal, :shutdown] do + {:stop, req, state} + end + + # expected error raised from user to return error immediately + def info({:EXIT, pid, {%RPCError{} = error, stacktrace}}, req, state = %{pid: pid}) do + req = send_error(req, error, state, :rpc_error) + + [req: req] + |> ReportException.new(error, stacktrace) + |> log_error(stacktrace) + + {:stop, req, state} + end + + # unknown error raised from rpc + def info({:EXIT, pid, {:handle_error, error}}, req, state = %{pid: pid}) do + %{kind: kind, reason: reason, stack: stack} = error + rpc_error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"} + req = send_error(req, rpc_error, state, :error) + + [req: req] + |> ReportException.new(reason, stack, kind) + |> log_error(stack) + + {:stop, req, state} + end + + def info({:EXIT, pid, {reason, stacktrace}}, req, state = %{pid: pid}) do + error = %RPCError{status: GRPC.Status.unknown(), message: "Internal Server Error"} + req = send_error(req, error, state, reason) + + [req: req] + |> ReportException.new(reason, stacktrace) + |> log_error(stacktrace) + + {:stop, req, state} + end + + def terminate(reason, _req, %{pid: pid}) do + exit_handler(pid, reason) + :ok + end + + def terminate(_reason, _req, _state) do + :ok + end + + def call_rpc(server, path, stream) do + result = + try do + case do_call_rpc(server, path, stream) do + {:error, _} = err -> + err + + _ -> + :ok + end + catch + kind, e -> + reason = Exception.normalize(kind, e, __STACKTRACE__) + + {:error, %{kind: kind, reason: reason, stack: __STACKTRACE__}} + end + + case result do + {:error, %GRPC.RPCError{} = e} -> + exit({e, _stacktrace = []}) + + {:error, %{kind: _kind, reason: _reason, stack: _stack} = e} -> + exit({:handle_error, e}) + + other -> + other + end + end + + defp do_call_rpc(server, path, %{http_method: http_method} = stream) do + result = server.__call_rpc__(path, http_method, stream) + + case result do + {:ok, stream, response} -> + stream + |> GRPC.Server.send_reply(response) + |> GRPC.Server.send_trailers(@default_trailers) + + {:ok, stream} + + {:ok, stream} -> + GRPC.Server.send_trailers(stream, @default_trailers) + {:ok, stream} + + error -> + error + end + end + + defp read_full_body(req, body, timer) do + result = :cowboy_req.read_body(req, timeout_left_opt(timer)) + + case result do + {:ok, data, req} -> {:ok, body <> data, req} + {:more, data, req} -> read_full_body(req, body <> data, timer) + end + end + + defp send_stream_trailers(req, trailers) do + req = check_sent_resp(req) + :cowboy_req.stream_trailers(trailers, req) + end + + defp check_sent_resp(%{has_sent_resp: _} = req) do + req + end + + defp check_sent_resp(req) do + :cowboy_req.stream_reply(200, req) + end + + defp send_error_trailers(%{has_sent_resp: _} = req, _, trailers) do + :cowboy_req.stream_trailers(trailers, req) + end + + defp send_error_trailers(req, status, trailers) do + :cowboy_req.reply(status, trailers, req) + end + + def exit_handler(pid, reason) do + if Process.alive?(pid) do + Process.exit(pid, reason) + end + end + + defp timeout_left_opt(timer, opts \\ %{}) do + case timer do + nil -> + Map.put(opts, :timeout, :infinity) + + timer -> + case Process.read_timer(timer) do + ms when is_integer(ms) -> + Map.put(opts, :timeout, ms) + + _ -> + Map.put(opts, :timeout, 0) + end + end + end + + defp extract_subtype("application/json"), do: {:ok, :http_transcoding, "json"} + defp extract_subtype("application/grpc"), do: {:ok, :grpc, "proto"} + defp extract_subtype("application/grpc+"), do: {:ok, :grpc, "proto"} + defp extract_subtype("application/grpc;"), do: {:ok, :grpc, "proto"} + defp extract_subtype(<<"application/grpc+", rest::binary>>), do: {:ok, :grpc, rest} + defp extract_subtype(<<"application/grpc;", rest::binary>>), do: {:ok, :grpc, rest} + + defp extract_subtype("application/grpc-web"), do: {:ok, :grpcweb, "proto"} + defp extract_subtype("application/grpc-web+"), do: {:ok, :grpcweb, "proto"} + defp extract_subtype("application/grpc-web;"), do: {:ok, :grpcweb, "proto"} + defp extract_subtype("application/grpc-web-text"), do: {:ok, :grpcweb, "text"} + defp extract_subtype("application/grpc-web+" <> rest), do: {:ok, :grpcweb, rest} + defp extract_subtype("application/grpc-web-text+" <> rest), do: {:ok, :grpcweb, rest} + + defp extract_subtype(type) do + Logger.warning("Got unknown content-type #{type}, please create an issue.") + {:ok, :grpc, "proto"} + end + + defp resolve_access_mode(%{version: "HTTP/1.1"}, _detected_access_mode, _type_subtype), + do: :http_transcoding + + defp resolve_access_mode(%{method: "OPTIONS"}, _detected_access_mode, _type_subtype), + do: :grpcweb + + defp resolve_access_mode(_req, detected_access_mode, _type_subtype), do: detected_access_mode + + defp preflight?(%{method: "OPTIONS"}), do: true + defp preflight?(_), do: false + + defp send_error(req, error, state, reason) do + trailers = HTTP2.server_trailers(error.status, error.message) + + status = + if state.access_mode == :http_transcoding, + do: GRPC.Status.http_code(error.status), + else: 200 + + if pid = Map.get(state, :pid) do + exit_handler(pid, reason) + end + + send_error_trailers(req, status, trailers) + end + + # Similar with cowboy's read_body, but we need to receive the message + # in `info` callback. + defp async_read_body(%{has_body: false} = req, _opts) do + {:send, {:ok, <<>>, req}} + end + + defp async_read_body(%{has_read_body: true} = req, _opts) do + {:send, {:ok, <<>>, req}} + end + + defp async_read_body(req, opts) do + length = opts[:length] || 8_000_000 + period = opts[:period] || 15000 + ref = make_ref() + + :cowboy_req.cast({:read_body, self(), ref, length, period}, req) + + {:wait, ref} + end + + defp log_error(%ReportException{kind: kind} = exception, stacktrace) do + crash_reason = GRPC.Logger.crash_reason(kind, exception, stacktrace) + + kind + |> Exception.format(exception, stacktrace) + |> Logger.error(crash_reason: crash_reason) + end +end diff --git a/lib/grpc/server/adapters/cowboy/router.ex b/lib/grpc/server/adapters/cowboy/router.ex new file mode 100644 index 000000000..2ed89b548 --- /dev/null +++ b/lib/grpc/server/adapters/cowboy/router.ex @@ -0,0 +1,143 @@ +defmodule GRPC.Server.Adapters.Cowboy.Router do + # Most of the functionality in this module is lifted from :cowboy_router, with the unused parts + # removed. Since the template language for Google.Api.HttpRule is quite rich, it cannot be expressed + # in terms of the default routing offered by cowboy. + # This module is configured to be used as middleware in `src/grpc_stream_h.erl` instead of :cowoby_router + @moduledoc false + @behaviour :cowboy_middleware + + alias GRPC.Server.Router + + def compile(routes) do + for {host, paths} <- routes do + [{host_match, _, _}] = :cowboy_router.compile([{host, []}]) + compiled_paths = compile_paths(paths, []) + + {host_match, [], compiled_paths} + end + end + + def compile_paths([], acc) do + Enum.reverse(acc) + end + + def compile_paths([{path, handler, opts} | paths], acc) when is_binary(path) do + {_, _, matches} = Router.build_route(path) + + compile_paths(paths, [{matches, [], handler, opts} | acc]) + end + + def compile_paths([{route, handler, opts} | paths], acc) do + compile_paths(paths, [{route, [], handler, opts} | acc]) + end + + @impl :cowboy_middleware + def execute( + req = %{host: host, path: path}, + env = %{dispatch: dispatch} + ) do + dispatch = + case dispatch do + {:persistent_term, key} -> + :persistent_term.get(key) + + _ -> + dispatch + end + + case match(dispatch, host, path) do + {:ok, handler, handler_opts, bindings, host_info, path_info} -> + {:ok, Map.merge(req, %{host_info: host_info, path_info: path_info, bindings: bindings}), + Map.merge(env, %{handler: handler, handler_opts: handler_opts})} + + {:error, :notfound, :host} -> + {:stop, :cowboy_req.reply(400, req)} + + {:error, :badrequest, :path} -> + {:stop, :cowboy_req.reply(400, req)} + + {:error, :notfound, :path} -> + {:stop, :cowboy_req.reply(404, req)} + end + end + + def match([], _, _) do + {:error, :notfound, :host} + end + + def match([{:_, [], path_matchs} | _Tail], _, path) do + match_path(path_matchs, :undefined, path, %{}) + end + + defp match_path([], _, _, _) do + {:error, :notfound, :path} + end + + defp match_path([{:_, [], handler, opts} | _Tail], host_info, _, bindings) do + {:ok, handler, opts, bindings, host_info, :undefined} + end + + defp match_path([{"*", _, handler, opts} | _Tail], host_info, "*", bindings) do + {:ok, handler, opts, bindings, host_info, :undefined} + end + + defp match_path([_ | tail], host_info, "*", bindings) do + match_path(tail, host_info, "*", bindings) + end + + defp match_path([{path_match, fields, handler, opts} | tail], host_info, tokens, bindings) + when is_list(tokens) do + case Router.match(tokens, path_match, bindings) do + false -> + match_path(tail, host_info, tokens, bindings) + + {true, path_binds} -> + case check_constraints(fields, path_binds) do + {:ok, path_binds} -> + {:ok, handler, opts, path_binds, host_info, :undefined} + + :nomatch -> + match_path(tail, host_info, tokens, bindings) + end + end + end + + defp match_path(_Dispatch, _HostInfo, :badrequest, _Bindings) do + {:error, :badrequest, :path} + end + + defp match_path(dispatch, host_info, path, bindings) do + match_path(dispatch, host_info, Router.split_path(path), bindings) + end + + defp check_constraints([], bindings) do + {:ok, bindings} + end + + defp check_constraints([field | tail], bindings) when is_atom(field) do + check_constraints(tail, bindings) + end + + defp check_constraints([field | tail], bindings) do + name = elem(field, 0) + + case bindings do + %{^name => value} -> + constraints = elem(field, 1) + + case :cowboy_constraints.validate( + value, + constraints + ) do + {:ok, value} -> + check_constraints(tail, Map.put(bindings, name, value)) + + {:error, _} -> + :nomatch + end + + _ -> + check_constraints(tail, bindings) + end + end +end diff --git a/lib/grpc/server/adapters/report_exception.ex b/lib/grpc/server/adapters/report_exception.ex new file mode 100644 index 000000000..47abe6e2b --- /dev/null +++ b/lib/grpc/server/adapters/report_exception.ex @@ -0,0 +1,31 @@ +defmodule GRPC.Server.Adapters.ReportException do + @moduledoc """ + Exception raised by server adapter, meant to be used as part of metadata `crash_report` + """ + + defexception [:kind, :reason, :stack, :adapter_extra] + + def new(adapter_extra, exception, stack \\ [], kind \\ :error) + + def new(adapter_extra, %{__exception__: _} = exception, stack, kind) do + exception(kind: kind, reason: exception, stack: stack, adapter_extra: adapter_extra) + end + + def new(adapter_extra, {erl_error, erl_stack}, _stack, kind) do + new(adapter_extra, Exception.normalize(:error, erl_error, erl_stack), erl_stack, kind) + end + + def new(adapter_extra, erl_error, stack, kind) do + new(adapter_extra, Exception.normalize(:error, erl_error, stack), stack, kind) + end + + def message(%__MODULE__{adapter_extra: [{:req, :ok}], kind: kind, reason: reason, stack: stack}) do + Exception.format_banner(kind, reason, stack) + end + + def message(%__MODULE__{adapter_extra: [{:req, req}], kind: kind, reason: reason, stack: stack}) do + # Cowboy adapter message builder + path = :cowboy_req.path(req) + "Exception raised while handling #{path}:\n" <> Exception.format_banner(kind, reason, stack) + end +end diff --git a/lib/grpc/server/interceptor.ex b/lib/grpc/server/interceptor.ex new file mode 100644 index 000000000..3e460cbdd --- /dev/null +++ b/lib/grpc/server/interceptor.ex @@ -0,0 +1,32 @@ +defmodule GRPC.ServerInterceptor do + @moduledoc """ + Interceptor on server side. See `GRPC.Endpoint`. + """ + + @moduledoc deprecated: "Use `GRPC.Server.Interceptor` instead" + + alias GRPC.Server.Stream + + @type options :: any() + @type rpc_return :: + {:ok, Stream.t(), struct()} | {:ok, Stream.t()} | {:error, GRPC.RPCError.t()} + @type next :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return()) + + @callback init(options) :: options + @callback call(GRPC.Server.rpc_req(), stream :: Stream.t(), next, options) :: rpc_return +end + +defmodule GRPC.Server.Interceptor do + @moduledoc """ + Interceptor on server side. See `GRPC.Endpoint`. + """ + alias GRPC.Server.Stream + + @type options :: any() + @type rpc_return :: + {:ok, Stream.t(), struct()} | {:ok, Stream.t()} | {:error, GRPC.RPCError.t()} + @type next :: (GRPC.Server.rpc_req(), Stream.t() -> rpc_return()) + + @callback init(options) :: options + @callback call(GRPC.Server.rpc_req(), stream :: Stream.t(), next, options) :: rpc_return +end diff --git a/lib/grpc/server/interceptors/cors.ex b/lib/grpc/server/interceptors/cors.ex new file mode 100644 index 000000000..d75b4fa91 --- /dev/null +++ b/lib/grpc/server/interceptors/cors.ex @@ -0,0 +1,121 @@ +defmodule GRPC.Server.Interceptors.CORS do + @moduledoc """ + Sends CORS headers when the client is using RPC via Web transcoding or gRPC-web. + + ## Options + + * `:allow_origin` - Required. A string containing the allowed origin, or a function capture (e.g. `&MyApp.MyModule.function/2)`) which takes a `req` and a `stream` and returns a string. + * `:allow_headers` - A string containing the allowed headers, or a function capture + (e.g. `&MyApp.MyModule.function/2)`) which takes a `req` and a `stream` and returns a string. Defaults to `nil`. + If defined as `nil`, the value of the `"access-control-request-headers"` request header from the client will be used in the response. + + ## Usage + + defmodule Your.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Server.Interceptors.CORS + end + + defmodule Your.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Server.Interceptors.CORS, allow_origin: "some.origin" + end + + + defmodule Your.Endpoint do + use GRPC.Endpoint + + def allow_origin(req, stream), do: "calculated.origin" + intercept GRPC.Server.Interceptors.CORS, allow: &Your.Endpoint.allow_origin/2 + end + """ + + @behaviour GRPC.Server.Interceptor + @impl true + def init(opts \\ []) do + # Function captures are represented as their AST in this step + # because of a Macro.escape call in the __before_compile__ step + # in endpoint.ex. + # This is not a full-on Macro context, so binary concatenations and + # variables are handled before this step. + + opts = Keyword.validate!(opts, [:allow_origin, allow_headers: nil]) + + allow_origin = + case opts[:allow_origin] do + {:&, [], [{:/, [], [_signature, 2]}]} = fun -> + fun + + binary when is_binary(binary) -> + binary + + other -> + raise ArgumentError, + "allow_origin must be a string or a 2-arity remote function, got: #{inspect(other)}" + end + + allow_headers = + case opts[:allow_headers] do + {:&, [], [{:/, [], [_signature, 2]}]} = fun -> + fun + + binary when is_binary(binary) -> + binary + + nil -> + nil + + other -> + raise ArgumentError, + ":allow_headers must be a string, a 2-arity remote function, or nil, got: #{inspect(other)}" + end + + {allow_origin, allow_headers} + end + + @impl true + def call(req, stream, next, {allow_origin, allow_headers}) do + if stream.access_mode != :grpc and + Map.get(stream.http_request_headers, "sec-fetch-mode") == "cors" do + headers = + %{} + |> add_allowed_origins(req, stream, allow_origin) + |> add_allowed_headers(req, stream, allow_headers) + + stream.adapter.set_headers(stream.payload, headers) + end + + next.(req, stream) + end + + defp add_allowed_origins(headers, req, stream, allow) do + value = + case allow do + allow when is_function(allow, 2) -> allow.(req, stream) + allow -> allow + end + + Map.put(headers, "access-control-allow-origin", value) + end + + defp add_allowed_headers( + headers, + req, + %{http_request_headers: %{"access-control-request-headers" => requested}} = stream, + allow + ) do + # include an access-control-allow-headers header only when a request headers is sent + value = + case allow do + nil -> requested + allow when is_function(allow, 2) -> allow.(req, stream) + allow -> allow + end + + Map.put(headers, "access-control-allow-headers", value) + end + + defp add_allowed_headers(headers, _req, _stream, _allowed), do: headers +end diff --git a/lib/grpc/server/interceptors/logger.ex b/lib/grpc/server/interceptors/logger.ex new file mode 100644 index 000000000..4b46cdcc8 --- /dev/null +++ b/lib/grpc/server/interceptors/logger.ex @@ -0,0 +1,56 @@ +defmodule GRPC.Server.Interceptors.Logger do + @moduledoc """ + Print log around server rpc calls, like: + + 17:18:45.151 [info] Handled by HelloServer.say_hello + 17:18:45.151 [info] Response :ok in 11µs + + ## Options + + * `:level` - the desired log level. Defaults to `:info` + + ## Usage + + defmodule Your.Endpoint do + use GRPC.Endpoint + + intercept GRPC.Server.Interceptors.Logger, level: :info + end + """ + + require Logger + + @behaviour GRPC.Server.Interceptor + + @impl true + def init(opts) do + Keyword.validate!(opts, level: :info) + end + + @impl true + def call(req, stream, next, opts) do + level = Keyword.fetch!(opts, :level) + + if Logger.compare_levels(level, Logger.level()) != :lt do + Logger.metadata(request_id: Logger.metadata()[:request_id] || stream.request_id) + + Logger.log(level, "Handled by #{inspect(stream.server)}.#{elem(stream.rpc, 0)}") + + start = System.monotonic_time() + result = next.(req, stream) + stop = System.monotonic_time() + + status = elem(result, 0) + diff = System.convert_time_unit(stop - start, :native, :microsecond) + + Logger.log(level, "Response #{inspect(status)} in #{formatted_diff(diff)}") + + result + else + next.(req, stream) + end + end + + def formatted_diff(diff) when diff > 1000, do: [diff |> div(1000) |> Integer.to_string(), "ms"] + def formatted_diff(diff), do: [Integer.to_string(diff), "µs"] +end diff --git a/lib/grpc/server/router.ex b/lib/grpc/server/router.ex new file mode 100644 index 000000000..38b003118 --- /dev/null +++ b/lib/grpc/server/router.ex @@ -0,0 +1,214 @@ +defmodule GRPC.Server.Router do + @moduledoc """ + """ + alias __MODULE__.Template + + @type http_method :: :get | :put | :post | :patch | :delete + @type route :: {http_method(), String.t(), Template.matchers()} + + @wildcards [:_, :__] + + @spec build_route(binary() | map()) :: route() + def build_route(path) when is_binary(path), do: build_route(:post, path) + def build_route(%{pattern: {method, path}}), do: build_route(method, path) + + @doc """ + Builds a t:route/0 from a URL path or `t:Google.Api.Http.t/0`. + + The matcher part in the route can be used in `match/3` to match on a URL path or a list of segments. + + ## Examples + + {:get, "/v1/messages/{message_id}", match} = GRPC.Server.Router.build_route(:get, "/v1/messages/{message_id}") + + {:get, path, match} = GRPC.Server.Router.build_route(:get, "/v1/{book.location=shelves/*}/books/{book.name=*}") + {true, %{"book.location": "shelves/example-shelf", "book.name": "example-book"}} = GRPC.Server.Router.match("/v1/shelves/example-shelf/books/example-book", match, []) + """ + @spec build_route(atom(), binary()) :: route() + def build_route(method, path) when is_binary(path) do + match = + path + |> Template.tokenize([]) + |> Template.parse([]) + + {method, path, match} + end + + @doc """ + Split URL path into segments, removing the leading and trailing slash. + + ## Examples + + ["v1", "messages"] = GRPC.Server.Router.split_path("/v1/messages") + """ + @spec split_path(String.t()) :: iolist() + def split_path(bin) do + for segment <- String.split(bin, "/"), segment != "", do: segment + end + + @doc """ + Matches a URL path or URL segements against a compiled route matcher. Matched bindings from the segments are extracted + into a map. If the same variable name is used in multiple bindings, the value must match otherwise the route is not considered a match. + + ## Examples + + {_, _, match} = GRPC.Server.Router.build_route(:get, "/v1/{name=messages}") + {true, %{name: "messages"}} = GRPC.Server.Router.match("/v1/messages", match) + false = GRPC.Server.Router.match("/v1/messages/foobar", match) + + + {_, _, match} = GRPC.Server.Router.build_route(:get, "/v1/{name=shelves/*/books/*) + {true, %{name: "shelves/books/book"}} = GRPC.Server.Router.match("/v1/shelves/example-shelf/books/book", match) + + false = GRPC.Server.Router.match("/v1/shelves/example-shelf/something-els/books/book", match) + + """ + @spec match(String.t() | [String.t()], Template.matchers()) :: {true, map()} | false + def match(path, match) do + match(path, match, %{}) + end + + @spec match(String.t() | [String.t()], Template.matchers(), map()) :: {true, map()} | false + def match(path, match, bindings) when is_binary(path) do + path + |> split_path() + |> match(match, bindings) + end + + # The last matcher is a 'catch all' matcher, so the rest of segments are matching. + def match(_segments, [{:__, []}], bindings) do + {true, bindings} + end + + # 'Any' matcher matches a single segment, cont. recursion. + def match([_s | segments], [{:_, _} | matchers], bindings) do + match(segments, matchers, bindings) + end + + # Matching against a 'literal' match, cont. recursion + def match([segment | segments], [_literal = segment | matchers], bindings) do + match(segments, matchers, bindings) + end + + # /v1/{a=*} is the same as /v1/{a}. Matching and binding the segment to `binding` + def match([segment | tail], [{binding, [{:_, _}]} | matchers], bindings) do + put_binding(bindings, binding, segment, tail, matchers) + end + + # /v1/{a=messages} binding a matching literal + def match([segment | segments], [{binding, [segment]} | matchers], bindings) do + put_binding(bindings, binding, segment, segments, matchers) + end + + # /v1/{a=*} /v1/{a=**} theres no more matchers after the wildcard, bind + # the rest of the segments to `binding` + def match(rest, [{binding, [{any, _}]}], bindings) when any in @wildcards do + value = Enum.join(rest, "/") + + match([], [], Map.put(bindings, binding, value)) + end + + # /v1/{a=messages/*} /v1/{a=messages/**} theres no more matchers after the wildcard, bind + # the rest of the segments including the current segment to `binding` + def match([segment | _] = segments, [{binding, [segment, {any, _}]}], bindings) + when any in @wildcards do + value = Enum.join(segments, "/") + + match([], [], Map.put(bindings, binding, value)) + end + + # /v1/{a=users/*/messages/*}/suffix. There are sub-matches inside the capture + # so the segments are matched with match submatches until an end-condition + # is reached + def match( + [segment | tail], + [{binding, [segment | sub_matches]} | matches], + bindings + ) do + end_condition = + case matches do + [next | _] -> next + [] -> :undefined + end + + with {matched_segments, tail} <- match_until(tail, end_condition, sub_matches, []) do + value = Enum.join([segment | matched_segments], "/") + bindings = Map.put(bindings, binding, value) + + match(tail, matches, bindings) + end + end + + # /v1/messages/{message_id} simple binding + def match([segment | segments], [{binding, []} | matchers], bindings) when is_atom(binding) do + put_binding(bindings, binding, segment, segments, matchers) + end + + def match([], [], bindings) do + {true, bindings} + end + + # no match + def match(_segments, _matches, _bindings) do + false + end + + # End recursion, since there's no "outside" matches we should iterate to end of segments + defp match_until([], :undefined, [], acc) do + {Enum.reverse(acc), []} + end + + # End recursion, end condition is a binding with a matching complex start segment + defp match_until( + [segment | _] = segments, + _end_condition = {binding, [segment | _]}, + [], + acc + ) + when is_atom(binding) do + {Enum.reverse(acc), segments} + end + + # End recursion since the submatch contains a trailing wildcard but we have more matches "outside" this sub-segment + defp match_until([segment | _] = segments, _end_condition = segment, [], acc) do + {Enum.reverse(acc), segments} + end + + # Reached the "end" of this wildcard, so we proceed with the next match + defp match_until([_segment | _] = segments, end_condition, [{:__, []}, match | matches], acc) do + match_until(segments, end_condition, [match | matches], acc) + end + + # Segment is matching the wildcard and have not reached "end" of wildcard + defp match_until([segment | segments], end_condition, [{:__, []} | _] = matches, acc) do + match_until(segments, end_condition, matches, [segment | acc]) + end + + # Current match is matching segment, add to accumulator and set next match as the current one + defp match_until([segment | segments], end_condition, [segment | matches], acc) do + match_until(segments, end_condition, matches, [segment | acc]) + end + + # 'Any' match is matching first segment, add to accumulator and set next match as the current one + defp match_until([segment | segments], end_condition, [{:_, []} | matches], acc) do + match_until(segments, end_condition, matches, [segment | acc]) + end + + # No match + defp match_until(_segments, _end_condition, _matches, _acc) do + false + end + + defp put_binding(bindings, binding, value, segments, matchers) do + case bindings do + %{^binding => ^value} -> + match(segments, matchers, bindings) + + %{^binding => _} -> + false + + _ -> + match(segments, matchers, Map.put(bindings, binding, value)) + end + end +end diff --git a/lib/grpc/server/router/field_path.ex b/lib/grpc/server/router/field_path.ex new file mode 100644 index 000000000..d82038ef1 --- /dev/null +++ b/lib/grpc/server/router/field_path.ex @@ -0,0 +1,20 @@ +defmodule GRPC.Server.Router.FieldPath do + @moduledoc false + + @spec decode_pair({binary(), term()}, map()) :: map() + def decode_pair({key, value}, acc) do + parts = :binary.split(key, ".", [:global]) + assign_map(parts, value, acc) + end + + defp assign_map(parts, value, acc) do + {_, acc} = + get_and_update_in(acc, Enum.map(parts, &Access.key(&1, %{})), fn + prev when prev == %{} -> {prev, value} + prev when is_list(prev) -> {prev, [value | prev]} + prev -> {prev, [value, prev]} + end) + + acc + end +end diff --git a/lib/grpc/server/router/query.ex b/lib/grpc/server/router/query.ex new file mode 100644 index 000000000..409ba269c --- /dev/null +++ b/lib/grpc/server/router/query.ex @@ -0,0 +1,50 @@ +defmodule GRPC.Server.Router.Query do + @moduledoc false + # This module is based on https://github.com/elixir-plug/plug/blob/main/lib/plug/conn/query.ex + # Decoding of URL-encoded queries as per the rules outlined in the documentation for [`google.api.HttpRule`](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule) + # It provides similar functionality to `URI.decode_query/3` or `Plug.Conn.Query.decode/4` with the following differences: + + # 1. A repeated key is treated as a list of values + # 1. Sub-paths on the form `path.subpath` are decoded as nested maps + # 1. Sub-paths with the same leaf key are decoded as a list + + alias GRPC.Server.Router.FieldPath + + @spec decode(String.t(), map()) :: %{optional(String.t()) => term()} + def decode(query, acc \\ %{}) + + def decode("", acc) do + acc + end + + def decode(query, acc) when is_binary(query) do + parts = :binary.split(query, "&", [:global]) + + Enum.reduce( + Enum.reverse(parts), + acc, + &decode_www_pair(&1, &2) + ) + end + + defp decode_www_pair("", acc) do + acc + end + + defp decode_www_pair(binary, acc) do + current = + case :binary.split(binary, "=") do + [key, value] -> + {decode_www_form(key), decode_www_form(value)} + + [key] -> + {decode_www_form(key), ""} + end + + FieldPath.decode_pair(current, acc) + end + + defp decode_www_form(value) do + URI.decode_www_form(value) + end +end diff --git a/lib/grpc/server/router/template.ex b/lib/grpc/server/router/template.ex new file mode 100644 index 000000000..15cd74590 --- /dev/null +++ b/lib/grpc/server/router/template.ex @@ -0,0 +1,102 @@ +defmodule GRPC.Server.Router.Template do + @moduledoc false + # https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule + # Template = "/" Segments [ Verb ] ; + # Segments = Segment { "/" Segment } ; + # Segment = "*" | "**" | LITERAL | Variable ; + # Variable = "{" FieldPath [ "=" Segments ] "}" ; + # FieldPath = IDENT { "." IDENT } ; + # Verb = ":" LITERAL ; + @type segment_match :: String.t() | {atom(), [segment_match]} + @type matchers :: [segment_match] + + @spec tokenize(binary(), [tuple()]) :: [tuple()] + def tokenize(path, tokens \\ []) + + def tokenize(<<>>, tokens) do + Enum.reverse(tokens) + end + + def tokenize(segments, tokens) do + {token, rest} = do_tokenize(segments, <<>>) + tokenize(rest, [token | tokens]) + end + + @terminals [?/, ?{, ?}, ?=, ?*] + defp do_tokenize(<>, <<>>) when h in @terminals do + {{List.to_atom([h]), []}, t} + end + + defp do_tokenize(<> = rest, acc) when h in @terminals do + {{:identifier, acc, []}, rest} + end + + defp do_tokenize(<>, acc) + when h in ?a..?z or h in ?A..?Z or h in ?0..?9 or h == ?_ or h == ?. do + do_tokenize(t, <>) + end + + defp do_tokenize(<<>>, acc) do + {{:identifier, acc, []}, <<>>} + end + + @spec parse(tokens :: [tuple()], matchers()) :: matchers() | {matchers, tokens :: [tuple()]} + def parse([], matchers) do + Enum.reverse(matchers) + end + + def parse([{:/, _} | rest], matchers) do + parse(rest, matchers) + end + + def parse([{:*, _}, {:*, _} | rest], matchers) do + parse(rest, [{:__, []} | matchers]) + end + + def parse([{:*, _} | rest], matchers) do + parse(rest, [{:_, []} | matchers]) + end + + def parse([{:identifier, identifier, _} | rest], matchers) do + parse(rest, [identifier | matchers]) + end + + def parse([{:"{", _} | rest], matchers) do + {matchers, rest} = parse_binding(rest, matchers) + parse(rest, matchers) + end + + def parse([{:"}", _} | _rest] = acc, matchers) do + {matchers, acc} + end + + defp parse_binding([], matchers) do + {matchers, []} + end + + defp parse_binding([{:"}", []} | rest], matchers) do + {matchers, rest} + end + + defp parse_binding( + [{:identifier, id, _}, {:=, _} | rest], + matchers + ) do + variable = field_path(id) + {assign, rest} = parse(rest, []) + + parse_binding(rest, [{variable, Enum.reverse(assign)} | matchers]) + end + + defp parse_binding([{:identifier, id, []} | rest], matchers) do + variable = field_path(id) + parse_binding(rest, [{variable, []} | matchers]) + end + + defp field_path(identifier) do + String.to_existing_atom(identifier) + rescue + _e -> + String.to_atom(identifier) + end +end diff --git a/lib/grpc/server/stream.ex b/lib/grpc/server/stream.ex index 9463ecf92..fa0a7c195 100644 --- a/lib/grpc/server/stream.ex +++ b/lib/grpc/server/stream.ex @@ -9,31 +9,40 @@ defmodule GRPC.Server.Stream do ## Fields * `:server` - user defined gRPC server module - * `:adapter` - a server adapter module, like `GRPC.Adapter.Cowboy` + * `:adapter` - a server adapter module, like `GRPC.Server.Adapters.Cowboy` * `request_mod` - the request module, or nil for untyped protocols * `response_mod` - the response module, or nil for untyped protocols * `:codec` - the codec * `:payload` - the payload needed by the adapter * `:local` - local data initialized by user """ + @type access_mode :: :grpc | :grpcweb | :http_transcoding @type t :: %__MODULE__{ - server: atom, + server: atom(), service_name: String.t(), method_name: String.t(), - grpc_type: atom, - endpoint: atom, - rpc: tuple, - request_mod: atom, - response_mod: atom, - codec: atom, - payload: any, - adapter: atom, - local: any, + grpc_type: atom(), + endpoint: atom(), + rpc: tuple(), + request_mod: atom(), + request_id: String.t() | nil, + response_mod: atom(), + codec: atom(), + payload: any(), + adapter: atom(), + local: any(), + access_mode: access_mode, # compressor mainly is used in client decompressing, responses compressing should be set by # `GRPC.Server.set_compressor` - compressor: module | nil, - __interface__: map + compressor: module() | nil, + # notes that this is a preflight request, and not an actual request for data (e.g. in grpcweb) + is_preflight?: boolean(), + # For http transcoding + http_method: GRPC.Server.Router.http_method(), + http_request_headers: map(), + http_transcode: boolean(), + __interface__: map() } defstruct server: nil, @@ -43,18 +52,59 @@ defmodule GRPC.Server.Stream do endpoint: nil, rpc: nil, request_mod: nil, + request_id: nil, response_mod: nil, codec: GRPC.Codec.Proto, payload: nil, adapter: nil, local: nil, + access_mode: :grpc, compressor: nil, + is_preflight?: false, + http_method: :post, + http_request_headers: %{}, + http_transcode: false, __interface__: %{send_reply: &__MODULE__.send_reply/3} - def send_reply(%{adapter: adapter, codec: codec} = stream, reply, opts) do - # {:ok, data, _size} = reply |> codec.encode() |> GRPC.Message.to_data() - data = codec.encode(reply) + def send_reply(%{is_preflight?: true} = stream, _reply, opts) do + do_send_reply(stream, [], opts) + end + + def send_reply( + %{grpc_type: :server_stream, codec: codec, access_mode: :http_transcoding, rpc: rpc} = + stream, + reply, + opts + ) do + rule = GRPC.Service.rpc_options(rpc, :http) || %{value: %{}} + response = GRPC.Server.Transcode.map_response_body(rule.value, reply) + + do_send_reply(stream, [codec.encode(response), "\n"], opts) + end + + def send_reply(%{codec: codec, access_mode: :http_transcoding, rpc: rpc} = stream, reply, opts) do + rule = GRPC.Service.rpc_options(rpc, :http) || %{value: %{}} + response = GRPC.Server.Transcode.map_response_body(rule.value, reply) + + do_send_reply(stream, codec.encode(response), opts) + end + + def send_reply(%{codec: codec} = stream, reply, opts) do + do_send_reply(stream, codec.encode(reply), opts) + end + + defp do_send_reply( + %{adapter: adapter, codec: codec, access_mode: access_mode} = stream, + data, + opts + ) do + opts = + opts + |> Keyword.put(:codec, codec) + |> Keyword.put(:http_transcode, access_mode == :http_transcoding) + adapter.send_reply(stream.payload, data, opts) + stream end end diff --git a/lib/grpc/server/supervisor.ex b/lib/grpc/server/supervisor.ex index c380a3e08..c22bcf4c5 100644 --- a/lib/grpc/server/supervisor.ex +++ b/lib/grpc/server/supervisor.ex @@ -1,55 +1,96 @@ defmodule GRPC.Server.Supervisor do - use Supervisor - @moduledoc """ A simple supervisor to start your servers. - You can add it to your OTP tree as below. But to make the servers start, you have to config `grpc` + You can add it to your OTP tree as below. + To start the server, you can pass `start_server: true` and an option defmodule Your.App do use Application def start(_type, _args) do - import Supervisor.Spec - children = [ - supervisor(GRPC.Server.Supervisor, [{Your.Endpoint, 50051(, opts)}]) - ] + {GRPC.Server.Supervisor, endpoint: Your.Endpoint, port: 50051, start_server: true, adapter_opts: [ip: {0, 0, 0, 0}], ...}] - opts = [strategy: :one_for_one, name: __MODULE__] - Supervisor.start_link(children, opts) + Supervisor.start_link(children, strategy: :one_for_one, name: __MODULE__) end end - # config.exs - config :grpc, start_server: true - or - run `mix grpc.server` on local - - View `child_spec/3` for opts. """ - @default_adapter GRPC.Adapter.Cowboy + use Supervisor + + # TODO: remove this type after support Elixir 1.14 exclusively. + @type sup_flags() :: %{ + strategy: Supervisor.strategy(), + intensity: non_neg_integer(), + period: pos_integer(), + auto_shutdown: Supervisor.auto_shutdown() + } + + @default_adapter GRPC.Server.Adapters.Cowboy require Logger def start_link(endpoint) do Supervisor.start_link(__MODULE__, endpoint) end - @spec init({module | [module], integer}) :: - {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore - def init({endpoint, port}) do - init({endpoint, port, []}) + @doc """ + ## Options + + * `:endpoint` - defines the endpoint module that will be started. + * `:port` - the HTTP port for the endpoint. + * `:servers` - the list of servers that will be be started. + * `:adapter_opts` - options for the adapter. + + Either `:endpoint` or `:servers` must be present, but not both. + """ + @spec init(tuple()) :: no_return + @spec init(keyword()) :: {:ok, {sup_flags(), [Supervisor.child_spec()]}} | :ignore + def init(opts) + + def init(opts) when is_tuple(opts) do + raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure." end - @spec init({module | [module], integer, Keyword.t()}) :: - {:ok, {:supervisor.sup_flags(), [:supervisor.child_spec()]}} | :ignore - def init({endpoint, port, opts}) do - check_deps_version() + def init(opts) when is_list(opts) do + unless is_nil(Application.get_env(:grpc, :start_server)) do + raise "the :start_server config key has been deprecated.\ + The currently supported way is to configure it\ + through the :start_server option for the GRPC.Server.Supervisor" + end + + opts = + case Keyword.validate(opts, [:endpoint, :servers, :start_server, :port, :adapter_opts]) do + {:ok, _opts} -> + opts + + {:error, _} -> + raise ArgumentError, + "just [:endpoint, :servers, :start_server, :port,] are accepted as arguments, and any other keys for adapters should be passed as adapter_opts!" + end + + endpoint_or_servers = + case {opts[:endpoint], opts[:servers]} do + {endpoint, servers} + when (not is_nil(endpoint) and not is_nil(servers)) or + (is_nil(endpoint) and is_nil(servers)) -> + raise ArgumentError, "either :endpoint or :servers must be passed, but not both." + + {endpoint, nil} -> + endpoint + + {nil, servers} when not is_list(servers) -> + raise ArgumentError, "either :servers must be a list of modules" + + {nil, servers} when is_list(servers) -> + servers + end children = - if Application.get_env(:grpc, :start_server, false) do - [child_spec(endpoint, port, opts)] + if opts[:start_server] do + [child_spec(endpoint_or_servers, opts[:port], opts)] else [] end @@ -64,9 +105,13 @@ defmodule GRPC.Server.Supervisor do * `:cred` - a credential created by functions of `GRPC.Credential`, an insecure server will be created without this option + * `:start_server` - determines if the server will be started. + If present, has more precedence then the `config :gprc, :start_server` + config value (i.e. `start_server: false` will not start the server in any case). """ - @spec child_spec(atom | [atom], integer, Keyword.t()) :: Supervisor.Spec.spec() - def child_spec(endpoint, port, opts \\ []) + @spec child_spec(endpoint_or_servers :: atom() | [atom()], port :: integer, opts :: keyword()) :: + Supervisor.Spec.spec() + def child_spec(endpoint_or_servers, port, opts \\ []) def child_spec(endpoint, port, opts) when is_atom(endpoint) do {endpoint, servers} = @@ -74,39 +119,21 @@ defmodule GRPC.Server.Supervisor do {endpoint, endpoint.__meta__(:servers)} rescue FunctionClauseError -> - Logger.warn( + Logger.warning( "deprecated: servers as argument of GRPC.Server.Supervisor, please use GRPC.Endpoint" ) {nil, endpoint} end - adapter = Keyword.get(opts, :adapter, @default_adapter) + adapter = Keyword.get(opts, :adapter) || @default_adapter servers = GRPC.Server.servers_to_map(servers) adapter.child_spec(endpoint, servers, port, opts) end def child_spec(servers, port, opts) when is_list(servers) do - adapter = Keyword.get(opts, :adapter, @default_adapter) + adapter = Keyword.get(opts, :adapter) || @default_adapter servers = GRPC.Server.servers_to_map(servers) adapter.child_spec(nil, servers, port, opts) end - - defp check_deps_version() do - # cowlib - case :application.get_key(:cowlib, :vsn) do - {:ok, vsn} -> - ver = to_string(vsn) - - unless Version.match?(ver, ">= 2.9.0") do - Logger.warn("cowlib should be >= 2.9.0, it's #{ver} now. See grpc's README for details") - end - - _ -> - :ok - end - rescue - _ -> - :ok - end end diff --git a/lib/grpc/server/transcode.ex b/lib/grpc/server/transcode.ex new file mode 100644 index 000000000..1d2562c5c --- /dev/null +++ b/lib/grpc/server/transcode.ex @@ -0,0 +1,81 @@ +defmodule GRPC.Server.Transcode do + @moduledoc false + alias GRPC.Server.Router.Query + alias GRPC.Server.Router.FieldPath + + @type t :: map() + # The request mapping follow the following rules: + # + # 1. Fields referred by the path template. They are passed via the URL path. + # 2. Fields referred by the HttpRule.body. They are passed via the HTTP request body. + # 3. All other fields are passed via the URL query parameters, and the parameter name is the field path in the request message. A repeated field can be represented as multiple query parameters under the same name. + # If HttpRule.body is "*", there is no URL query parameter, all fields are passed via URL path and HTTP request body. + # If HttpRule.body is omitted, there is no HTTP request body, all fields are passed via URL path and URL query parameters. + @spec map_request(t(), map(), map(), String.t(), module()) :: + {:ok, struct()} | {:error, term()} + def map_request( + %{body: ""}, + _body_request, + path_bindings, + query_string, + req_mod + ) do + path_bindings = map_path_bindings(path_bindings) + query = Query.decode(query_string) + request = Map.merge(path_bindings, query) + + Protobuf.JSON.from_decoded(request, req_mod) + end + + def map_request( + %{body: "*"} = rule, + body_request, + path_bindings, + _query_string, + req_mod + ) do + path_bindings = map_path_bindings(path_bindings) + body_request = map_request_body(rule, body_request) + request = Map.merge(path_bindings, body_request) + + Protobuf.JSON.from_decoded(request, req_mod) + end + + def map_request( + %{} = rule, + body_request, + path_bindings, + query_string, + req_mod + ) do + path_bindings = map_path_bindings(path_bindings) + query = Query.decode(query_string) + body_request = map_request_body(rule, body_request) + request = Enum.reduce([query, body_request], path_bindings, &Map.merge(&2, &1)) + + Protobuf.JSON.from_decoded(request, req_mod) + end + + defp map_request_body(%{body: "*"}, request_body), do: request_body + defp map_request_body(%{body: ""}, request_body), do: request_body + + defp map_request_body(%{body: field}, request_body), + do: %{field => request_body} + + @spec map_response_body(t() | map(), map()) :: map() + def map_response_body(%{response_body: ""}, response_body), do: response_body + + def map_response_body(%{response_body: field}, response_body) do + key = String.to_existing_atom(field) + Map.get(response_body, key) + end + + @spec map_path_bindings(map()) :: map() + def map_path_bindings(bindings) when bindings == %{}, do: bindings + + def map_path_bindings(bindings) do + for {k, v} <- bindings, reduce: %{} do + acc -> FieldPath.decode_pair({to_string(k), v}, acc) + end + end +end diff --git a/lib/grpc/service.ex b/lib/grpc/service.ex index 5357b6c22..e8ceda2b1 100644 --- a/lib/grpc/service.ex +++ b/lib/grpc/service.ex @@ -3,7 +3,7 @@ defmodule GRPC.Service do Define gRPC service used by Stub and Server. You should use `Protobuf` to to generate code instead of using this module directly. - It imports DSL functions like `rpc/3` and `stream/1` for defining the RPC + It imports DSL functions like `rpc/4` and `stream/1` for defining the RPC functions easily: defmodule Greeter.Service do @@ -11,11 +11,34 @@ defmodule GRPC.Service do rpc :SayHello, HelloRequest, stream(HelloReply) end + + `option (google.api.http)` annotations are supported for gRPC http/json transcoding. Once generated the 4th argument to `rpc/4` contains + the `Google.Api.HttpRule` option. + + defmodule Greeter.Service do + use GRPC.Service, name: "helloworld.Greeter" + + rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + __unknown_fields__: [], + additional_bindings: [], + body: "", + pattern: {:get, "/v1/greeter/{name}"}, + response_body: "", + selector: "" + } + } + }) + end """ defmacro __using__(opts) do + opts = Keyword.validate!(opts, [:name, :protoc_gen_elixir_version]) + quote do - import GRPC.Service, only: [rpc: 3, stream: 1] + import GRPC.Service, only: [rpc: 4, rpc: 3, stream: 1] Module.register_attribute(__MODULE__, :rpc_calls, accumulate: true) @before_compile GRPC.Service @@ -32,9 +55,10 @@ defmodule GRPC.Service do end end - defmacro rpc(name, request, reply) do + defmacro rpc(name, request, reply, options \\ quote(do: %{})) do quote do - @rpc_calls {unquote(name), unquote(wrap_stream(request)), unquote(wrap_stream(reply))} + @rpc_calls {unquote(name), unquote(wrap_stream(request)), unquote(wrap_stream(reply)), + unquote(options)} end end @@ -54,8 +78,11 @@ defmodule GRPC.Service do quote do: {unquote(param), false} end - def grpc_type({_, {_, false}, {_, false}}), do: :unary - def grpc_type({_, {_, true}, {_, false}}), do: :client_stream - def grpc_type({_, {_, false}, {_, true}}), do: :server_stream - def grpc_type({_, {_, true}, {_, true}}), do: :bidi_stream + def grpc_type({_, {_, false}, {_, false}, _}), do: :unary + def grpc_type({_, {_, true}, {_, false}, _}), do: :client_stream + def grpc_type({_, {_, false}, {_, true}, _}), do: :server_stream + def grpc_type({_, {_, true}, {_, true}, _}), do: :bidirectional_stream + + def rpc_options({_, _, _, options}), do: options + def rpc_options({_, _, _, options}, type), do: Map.get(options, type) end diff --git a/lib/grpc/status.ex b/lib/grpc/status.ex index 9c2b7e40e..80daf66eb 100644 --- a/lib/grpc/status.ex +++ b/lib/grpc/status.ex @@ -1,8 +1,28 @@ defmodule GRPC.Status do @moduledoc """ - Collection of gRPC status. - - Please refer to https://github.com/grpc/grpc/blob/master/include/grpc/impl/codegen/status.h + A collection of gRPC statuses. + + | Code | Status | Status Message | + |-------:|--------------------------|-----------------------------------------------------------------------------------------------------| + | `0` | `:ok` | Success | + | `1` | `:cancelled` | The operation was cancelled (typically by the caller) | + | `2` | `:unknown` | Unknown error | + | `3` | `:invalid_argument` | Client specified an invalid argument | + | `4` | `:deadline_exceeded` | Deadline expired before operation could complete | + | `5` | `:not_found` | Some requested entity (e.g., file or directory) was not found | + | `6` | `:already_exists` | Some entity that we attempted to create (e.g., file or directory) already exists | + | `7` | `:permission_denied` | The caller does not have permission to execute the specified operation | + | `8` | `:resource_exhausted` | Some resource has been exhausted | + | `9` | `:failed_precondition` | Operation was rejected because the system is not in a state required for the operation's execution | + | `10` | `:aborted` | The operation was aborted | + | `11` | `:out_of_range` | Operation was attempted past the valid range | + | `12` | `:unimplemented` | Operation is not implemented or not supported/enabled in this service | + | `13` | `:internal` | Internal errors | + | `14` | `:unavailable` | The service is currently unavailable | + | `15` | `:data_loss` | Unrecoverable data loss or corruption | + | `16` | `:unauthenticated` | The request does not have valid authentication credentials for the operation | + + For more details, please refer to the [official gRPC docs](https://github.com/grpc/grpc/blob/master/doc/statuscodes.md) """ @type t :: non_neg_integer @@ -10,11 +30,13 @@ defmodule GRPC.Status do @doc """ Not an error; returned on success. """ + @spec ok :: t def ok, do: 0 @doc """ The operation was cancelled (typically by the caller). """ + @spec cancelled() :: t() def cancelled, do: 1 @doc """ @@ -22,10 +44,11 @@ defmodule GRPC.Status do An example of where this error may be returned is if a Status value received from another address space belongs to - an error-space that is not known in this address space. Also + an error-space that is not known in this address space. Also errors raised by APIs that do not return enough error information may be converted to this error. """ + @spec unknown :: t def unknown, do: 2 @doc """ @@ -35,6 +58,7 @@ defmodule GRPC.Status do INVALID_ARGUMENT indicates arguments that are problematic regardless of the state of the system (e.g., a malformed file name). """ + @spec invalid_argument :: t def invalid_argument, do: 3 @doc """ @@ -45,16 +69,19 @@ defmodule GRPC.Status do successful response from a server could have been delayed long enough for the deadline to expire. """ + @spec deadline_exceeded :: t def deadline_exceeded, do: 4 @doc """ Some requested entity (e.g., file or directory) was not found. """ + @spec not_found :: t def not_found, do: 5 @doc """ Some entity that we attempted to create (e.g., file or directory) already exists. """ + @spec already_exists :: t def already_exists, do: 6 @doc """ @@ -67,12 +94,14 @@ defmodule GRPC.Status do used if the caller can not be identified (use UNAUTHENTICATED instead for those errors). """ + @spec permission_denied :: t def permission_denied, do: 7 @doc """ Some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space. """ + @spec resource_exhausted :: t def resource_exhausted, do: 8 @doc """ @@ -82,6 +111,7 @@ defmodule GRPC.Status do For example, directory to be deleted may be non-empty, an rmdir operation is applied to a non-directory, etc. """ + @spec failed_precondition :: t def failed_precondition, do: 9 @doc """ @@ -90,6 +120,7 @@ defmodule GRPC.Status do Typically due to a concurrency issue like sequencer check failures, transaction aborts, etc. """ + @spec aborted() :: t() def aborted, do: 10 @doc """ @@ -97,11 +128,13 @@ defmodule GRPC.Status do E.g., seeking or reading past end of file. """ + @spec out_of_range :: t def out_of_range, do: 11 @doc """ Operation is not implemented or not supported/enabled in this service. """ + @spec unimplemented :: t def unimplemented, do: 12 @doc """ @@ -110,6 +143,7 @@ defmodule GRPC.Status do Means some invariants expected by underlying system has been broken. If you see one of these errors, something is very broken. """ + @spec internal :: t def internal, do: 13 @doc """ @@ -118,18 +152,22 @@ defmodule GRPC.Status do This is a most likely a transient condition and may be corrected by retrying with a backoff. """ + @spec unavailable :: t def unavailable, do: 14 @doc """ Unrecoverable data loss or corruption. """ + @spec data_loss :: t def data_loss, do: 15 @doc """ The request does not have valid authentication credentials for the operation. """ + @spec unauthenticated :: t def unauthenticated, do: 16 + @spec code_name(t()) :: binary() def code_name(0), do: "OK" def code_name(1), do: "Canceled" def code_name(2), do: "Unknown" @@ -147,4 +185,56 @@ defmodule GRPC.Status do def code_name(14), do: "Unavailable" def code_name(15), do: "DataLoss" def code_name(16), do: "Unauthenticated" + + @spec http_code(t()) :: t() + def http_code(0), do: 200 + def http_code(1), do: 400 + def http_code(2), do: 500 + def http_code(3), do: 400 + def http_code(4), do: 504 + def http_code(5), do: 404 + def http_code(6), do: 409 + def http_code(7), do: 403 + def http_code(8), do: 429 + def http_code(9), do: 412 + def http_code(10), do: 409 + def http_code(11), do: 400 + def http_code(12), do: 501 + def http_code(13), do: 500 + def http_code(14), do: 503 + def http_code(15), do: 500 + def http_code(16), do: 401 + + @spec status_message(t()) :: String.t() | nil + def status_message(0), do: nil + def status_message(1), do: "The operation was cancelled (typically by the caller)" + def status_message(2), do: "Unknown error" + def status_message(3), do: "Client specified an invalid argument" + def status_message(4), do: "Deadline expired before operation could complete" + def status_message(5), do: "Some requested entity (e.g., file or directory) was not found" + + def status_message(6), + do: "Some entity that we attempted to create (e.g., file or directory) already exists" + + def status_message(7), + do: "The caller does not have permission to execute the specified operation" + + def status_message(8), do: "Some resource has been exhausted" + + def status_message(9), + do: + "Operation was rejected because the system is not in a state required for the operation's execution" + + def status_message(10), do: "The operation was aborted" + def status_message(11), do: "Operation was attempted past the valid range" + + def status_message(12), + do: "Operation is not implemented or not supported/enabled in this service" + + def status_message(13), do: "Internal errors" + def status_message(14), do: "The service is currently unavailable" + def status_message(15), do: "Unrecoverable data loss or corruption" + + def status_message(16), + do: "The request does not have valid authentication credentials for the operation" end diff --git a/lib/grpc/stream.ex b/lib/grpc/stream.ex index 06fb28fb3..99b37abec 100644 --- a/lib/grpc/stream.ex +++ b/lib/grpc/stream.ex @@ -1,17 +1,375 @@ defmodule GRPC.Stream do @moduledoc """ - Some useful operations for streams. + Provides a `Flow`-based abstraction layer for building gRPC streaming pipelines in Elixir. + + This module allows you to consume gRPC request streams as `Flow` pipelines with support for + backpressure via GenStage. You can also produce gRPC responses by materializing a `Flow` + back into the gRPC stream. + + ## Capabilities + + - Transforms an incoming gRPC request stream into a `Flow` with backpressure. + - Emits messages back into the gRPC response stream using `run_with/3`. + - Supports joining with external producers (e.g., RabbitMQ, Kafka) for unbounded or fan-in stream sources. + - Offers composable functional operators (`map/2`, `filter/2`, `flat_map/2`, etc.) on the stream. + + ## Example: Bidirectional Streaming + + defmodule MyGRPCService do + use GRPC.Server, service: MyService.Service + + def route_chat(input, materializer) do + GRPC.Stream.from(input, max_demand: 10) + |> GRPC.Stream.map(fn note -> process_note(note) end) + |> GRPC.Stream.run_with(materializer) + end + + defp process_note(note), do: %Response{message: "Received"} + end + + ## Example: Joining with an External Producer + + When integrating with external unbounded sources (e.g., message queues), + you can pass a running `GenStage` producer using the `:join_with` option: + + defmodule MyGRPCService do + use GRPC.Server, service: MyService.Service + + def stream_events(input, materializer) do + {:ok, pid} = MyApp.RabbitMQ.Producer.start_link([]) + + GRPC.Stream.from(input, join_with: pid, max_demand: 10) + |> GRPC.Stream.map(&handle_event/1) + |> GRPC.Stream.run_with(materializer) + end + + defp handle_event({_, msg}), do: msg + defp handle_event(event), do: %MyGRPC.Event{data: inspect(event)} + end + """ + alias GRPC.Stream.Operators + alias GRPC.Server.Stream, as: Materializer + + defstruct flow: nil, options: [], metadata: %{} + + @type t :: %__MODULE__{flow: Flow.t(), options: Keyword.t(), metadata: map()} + + @type item :: any() + + @type reason :: any() + + @doc """ + Converts a gRPC input into a `Flow` pipeline with backpressure support. + + ## Parameters + + - `input`: A gRPC request stream (struct, enumerable, or Elixir `Stream`). + + ## Options + + - `:join_with` — An optional external `GenStage` producer to merge with the gRPC input. + - `:dispatcher` — Specifies the `Flow` dispatcher (defaults to `GenStage.DemandDispatcher`). + - `:propagate_context` - If `true`, the context from the `materializer` is propagated to the `Flow`. + - `:materializer` - The `%GRPC.Server.Stream{}` struct representing the current gRPC stream context. + + And any other options supported by `Flow`. + + ## Returns + + A `GRPC.Stream` struct that represents the transformed stream. + + ## Example + + flow = GRPC.Stream.from(request, max_demand: 50) + """ + @spec from(any(), Keyword.t()) :: t() + def from(input, opts \\ []) + + def from(%Elixir.Stream{} = input, opts), do: build_grpc_stream(input, opts) + + def from(input, opts) when is_list(input), do: build_grpc_stream(input, opts) + + def from(input, opts) when not is_nil(input), do: from([input], opts) + + @doc """ + Converts a single gRPC request into a `Flow` pipeline with support for backpressure. + This is useful for unary gRPC requests where you want to use the Flow API. + + ## Parameters + + - `input`: The single gRPC message to convert into a Flow. + + ## Options + + - `:join_with` - An optional additional producer stage PID to include in the Flow. + - `:dispatcher` - An optional `GenStage` dispatcher to use in the underlying `Flow`. Defaults to `GenStage.DemandDispatcher`. + - `:propagate_context` - If `true`, the context from the `materializer` is propagated to the `Flow`. + - `:materializer` - The `%GRPC.Server.Stream{}` struct representing the current gRPC stream context. + + And any other options supported by `Flow`. + + ## Returns + - A `GRPCStream` that emits the single gRPC message under demand. + + ## Example + + flow = GRPCStream.single(request, max_demand: 5) + """ + @spec unary(any(), Keyword.t()) :: t() + def unary(input, opts \\ []) when is_struct(input), + do: build_grpc_stream([input], Keyword.merge(opts, unary: true)) + + @doc """ + Extracts the underlying `Flow` pipeline from a `GRPC.Stream`. + + Raises an `ArgumentError` if the `Flow` has not been initialized. + + ## Returns + + A `Flow` pipeline. + """ + @spec to_flow(t()) :: Flow.t() + def to_flow(%__MODULE__{flow: flow}) when is_nil(flow), do: Flow.from_enumerable([]) + + def to_flow(%__MODULE__{flow: flow}), do: flow + + @doc """ + Executes the underlying `Flow` for unary streams and emits responses into the provided gRPC server stream. + + ## Parameters + + - `flow`: A `GRPC.Stream` struct containing the flow to be executed. + - `stream`: A `GRPC.Server.Stream` to which responses are sent. + - `:dry_run` — If `true`, responses are not sent (used for testing or inspection). + + ## Example + + GRPC.Stream.run(request) + """ + @spec run(t()) :: any() + def run(%__MODULE__{flow: flow, options: opts}) do + unless Keyword.get(opts, :unary, false) do + raise ArgumentError, "run/2 is not supported for non-unary streams" + end + + # We have to call `Enum.to_list` because we want to actually run and materialize the full stream. + # List.flatten and List.first are used so that we can obtain the first result of the materialized list. + flow + |> Enum.to_list() + |> List.flatten() + |> List.first() + end + + @doc """ + Executes the flow and emits responses into the provided gRPC server stream. + + ## Parameters + + - `flow`: A `GRPC.Stream` struct containing the flow to be executed. + - `stream`: A `GRPC.Server.Stream` to which responses are sent. + + ## Options + + - `:dry_run` — If `true`, responses are not sent (used for testing or inspection). + + ## Returns + + - `:ok` if the stream was processed successfully. + + ## Example + + GRPC.Stream.run_with(request, mat) """ + @spec run_with(t(), Stream.t(), Keyword.t()) :: :ok + def run_with( + %__MODULE__{flow: flow, options: flow_opts} = _stream, + %Materializer{} = from, + opts \\ [] + ) do + if not Keyword.get(flow_opts, :unary, true) do + raise ArgumentError, "run_with/3 is not supported for unary streams" + end + + dry_run? = Keyword.get(opts, :dry_run, false) + + flow + |> Flow.map(fn msg -> + if not dry_run? do + send_response(from, msg) + end + + flow + end) + |> Flow.run() + end + + @doc """ + Sends a request to an external process and awaits a response. + + If `target` is a PID, a message in the format `{:request, item, from}` is sent, and a reply + in the format `{:response, msg}` is expected. + + If `target` is an `atom` we will try to locate the process through `Process.whereis/1`. + + ## Parameters + + - `stream`: The `GRPC.Stream` pipeline. + - `target`: Target process PID or atom name. + - `timeout`: Timeout in milliseconds (defaults to `5000`). + + ## Returns + + - Updated stream if successful. + - `{:error, item, reason}` if the request fails or times out. + """ + @spec ask(t(), pid | atom, non_neg_integer) :: t() | {:error, item(), reason()} + defdelegate ask(stream, target, timeout \\ 5000), to: Operators + + @doc """ + Same as `ask/3`, but raises an exception on failure. + + ## Caution + + This version propagates errors via raised exceptions, which can crash the Flow worker and halt the pipeline. + Prefer `ask/3` for production usage unless failure should abort the stream. + """ + @spec ask!(t(), pid | atom, non_neg_integer) :: t() + defdelegate ask!(stream, target, timeout \\ 5000), to: Operators @doc """ - Get headers from server stream. + Filters the stream using the given predicate function. + + The filter function is applied concurrently to the stream entries, so it shouldn't rely on execution order. + """ + @spec filter(t(), (term -> term)) :: t + defdelegate filter(stream, filter), to: Operators + + @doc """ + Applies a function to each entry and concatenates the resulting lists. + + Useful for emitting multiple messages for each input. + """ + @spec flat_map(t, (term -> Enumerable.t())) :: t() + defdelegate flat_map(stream, flat_mapper), to: Operators + + @doc """ + Applies a function to each stream item. + """ + @spec map(t(), (term -> term)) :: t() + defdelegate map(stream, mapper), to: Operators + + @doc """ + Applies a transformation function to each stream item, passing the context as an additional argument. + This is useful for operations that require access to the stream's headers. + """ + @spec map_with_context(t(), (map(), term -> term)) :: t() + defdelegate map_with_context(stream, mapper), to: Operators + + @doc """ + Partitions the stream to allow grouping of items by key or condition. + + Use this before stateful operations such as `reduce/3`. + + ## Note + + Excessive use of partitioning can impact performance and memory usage. + Only partition when required for correctness or performance. + See https://hexdocs.pm/flow/Flow.html#module-partitioning for more details. - For the client side, you should use `:return_headers` option to get headers, - see `GRPC.Stub` for details. + """ + @spec partition(t(), keyword()) :: t() + defdelegate partition(stream, options \\ []), to: Operators + + @doc """ + Reduces items in the stream using an accumulator. + + ## Parameters + + - `acc_fun` initializes the accumulator, + - `reducer_fun` updates it for each input. + + ## Note + See https://hexdocs.pm/flow/Flow.html#reduce/3 for more details. + + """ + @spec reduce(t, (-> acc), (term(), acc -> acc)) :: t when acc: term() + defdelegate reduce(stream, acc_fun, reducer_fun), to: Operators + + @doc """ + Emits only distinct items from the stream. See `uniq_by/2` for more information. + + """ + @spec uniq(t) :: t + defdelegate uniq(stream), to: Operators + + @doc """ + Emits only unique items as determined by the result of the given function. + + ## Note + This function requires care when used for unbounded flows. For more information see https://hexdocs.pm/flow/Flow.html#uniq_by/2 + + """ + @spec uniq_by(t, (term -> term)) :: t + defdelegate uniq_by(stream, fun), to: Operators + + @doc """ + Retrieves HTTP/2 headers from a `GRPC.Server.Stream`. + + ## Client Note + + To receive headers on the client side, use the `:return_headers` option. See `GRPC.Stub`. """ @spec get_headers(GRPC.Server.Stream.t()) :: map def get_headers(%GRPC.Server.Stream{adapter: adapter} = stream) do headers = adapter.get_headers(stream.payload) GRPC.Transport.HTTP2.decode_headers(headers) end + + defp build_grpc_stream(input, opts) do + metadata = + if Keyword.has_key?(opts, :propagate_context) do + %GRPC.Server.Stream{} = mat = Keyword.fetch!(opts, :materializer) + get_headers(mat) || %{} + end + + opts = Keyword.merge(opts, metadata: metadata) + dispatcher = Keyword.get(opts, :default_dispatcher, GenStage.DemandDispatcher) + + flow = + case Keyword.get(opts, :join_with) do + pid when is_pid(pid) -> + opts = Keyword.drop(opts, [:join_with, :default_dispatcher]) + + input_flow = Flow.from_enumerable(input, opts) + other_flow = Flow.from_stages([pid], opts) + Flow.merge([input_flow, other_flow], dispatcher, opts) + + name when not is_nil(name) and is_atom(name) -> + pid = Process.whereis(name) + + if not is_nil(pid) do + opts = Keyword.drop(opts, [:join_with, :default_dispatcher]) + + input_flow = Flow.from_enumerable(input, opts) + other_flow = Flow.from_stages([pid], opts) + Flow.merge([input_flow, other_flow], dispatcher, opts) + else + raise ArgumentError, "No process found for the given name: #{inspect(name)}" + end + + # handle Elixir.Stream joining + other when is_list(other) or is_function(other) -> + Flow.from_enumerables([input, other], opts) + + _ -> + opts = Keyword.drop(opts, [:join_with, :default_dispatcher]) + Flow.from_enumerable(input, opts) + end + + %__MODULE__{flow: flow, options: opts} + end + + defp send_response(from, msg) do + GRPC.Server.send_reply(from, msg) + end end diff --git a/lib/grpc/stream/operators.ex b/lib/grpc/stream/operators.ex new file mode 100644 index 000000000..1f3e15bb7 --- /dev/null +++ b/lib/grpc/stream/operators.ex @@ -0,0 +1,103 @@ +defmodule GRPC.Stream.Operators do + @moduledoc """ + Useful and internal functions for manipulating streams. + """ + alias GRPC.Stream, as: GRPCStream + + @type item :: any() + + @type reason :: any() + + @spec ask(GRPCStream.t(), pid | atom, non_neg_integer) :: + GRPCStream.t() | {:error, any(), :timeout | :not_alive} + def ask(%GRPCStream{flow: flow} = stream, target, timeout \\ 5000) do + mapper = fn item -> do_ask(item, target, timeout, raise_on_error: false) end + %GRPCStream{stream | flow: Flow.map(flow, mapper)} + end + + @spec ask!(GRPCStream.t(), pid | atom, non_neg_integer) :: GRPCStream.t() + def ask!(%GRPCStream{flow: flow} = stream, target, timeout \\ 5000) do + mapper = fn item -> do_ask(item, target, timeout, raise_on_error: true) end + %GRPCStream{stream | flow: Flow.map(flow, mapper)} + end + + defp do_ask(item, target, timeout, raise_on_error: raise?) do + resolved_target = + case target do + pid when is_pid(pid) -> if Process.alive?(pid), do: pid, else: nil + atom when is_atom(atom) -> Process.whereis(atom) + end + + cond do + is_nil(resolved_target) and raise? -> + raise "Target #{inspect(target)} is not alive. Cannot send request to it." + + is_nil(resolved_target) -> + {:error, item, :not_alive} + + true -> + send(resolved_target, {:request, item, self()}) + + receive do + {:response, res} -> res + after + timeout -> + if raise? do + raise "Timeout waiting for response from #{inspect(target)}" + else + {:error, item, :timeout} + end + end + end + end + + @spec filter(GRPCStream.t(), (term -> term)) :: GRPCStream.t() + def filter(%GRPCStream{flow: flow} = stream, filter) do + %GRPCStream{stream | flow: Flow.filter(flow, filter)} + end + + @spec flat_map(GRPCStream.t(), (term -> Enumerable.GRPCStream.t())) :: GRPCStream.t() + def flat_map(%GRPCStream{flow: flow} = stream, flat_mapper) do + %GRPCStream{stream | flow: Flow.flat_map(flow, flat_mapper)} + end + + @spec map(GRPCStream.t(), (term -> term)) :: GRPCStream.t() + def map(%GRPCStream{flow: flow} = stream, mapper) do + %GRPCStream{stream | flow: Flow.map(flow, mapper)} + end + + @spec map_with_context(GRPCStream.t(), (map(), term -> term)) :: GRPCStream.t() + def map_with_context(%GRPCStream{flow: flow, metadata: meta} = stream, mapper) + when is_function(mapper, 2) do + wrapper = fn item -> + mapper.(meta, item) + end + + %GRPCStream{stream | flow: Flow.map(flow, wrapper)} + end + + @spec partition(GRPCStream.t(), keyword()) :: GRPCStream.t() + def partition(%GRPCStream{flow: flow} = stream, options \\ []) do + %GRPCStream{stream | flow: Flow.partition(flow, options)} + end + + @spec reduce(GRPCStream.t(), (-> acc), (term, acc -> acc)) :: GRPCStream.t() when acc: term() + def reduce(%GRPCStream{flow: flow} = stream, acc_fun, reducer_fun) do + %GRPCStream{stream | flow: Flow.reduce(flow, acc_fun, reducer_fun)} + end + + @spec reject(GRPCStream.t(), (term -> term)) :: GRPCStream.t() + def reject(%GRPCStream{flow: flow} = stream, filter) do + %GRPCStream{stream | flow: Flow.reject(flow, filter)} + end + + @spec uniq(GRPCStream.t()) :: GRPCStream.t() + def uniq(%GRPCStream{flow: flow} = stream) do + %GRPCStream{stream | flow: Flow.uniq(flow)} + end + + @spec uniq_by(GRPCStream.t(), (term -> term)) :: GRPCStream.t() + def uniq_by(%GRPCStream{flow: flow} = stream, fun) do + %GRPCStream{stream | flow: Flow.uniq_by(flow, fun)} + end +end diff --git a/lib/grpc/stub.ex b/lib/grpc/stub.ex index de5a8e1c2..c2e100d23 100644 --- a/lib/grpc/stub.ex +++ b/lib/grpc/stub.ex @@ -3,7 +3,7 @@ defmodule GRPC.Stub do A module acting as the interface for gRPC client. You can do everything in the client side via `GRPC.Stub`, including connecting, - sending/receiving steaming or non-steaming requests, canceling calls and so on. + sending/receiving streaming or non-streaming requests, canceling calls and so on. A service is needed to define a stub: @@ -44,22 +44,28 @@ defmodule GRPC.Stub do # 10 seconds @default_timeout 10000 - @type rpc_return :: - {:ok, struct} - | {:ok, struct, map} - | GRPC.Client.Stream.t() + @type receive_data_return :: + {:ok, struct()} + | {:ok, struct(), map()} | {:ok, Enumerable.t()} - | {:ok, Enumerable.t(), map} + | {:ok, Enumerable.t(), map()} + + @type rpc_return :: + GRPC.Client.Stream.t() | {:error, GRPC.RPCError.t()} + | receive_data_return require Logger defmacro __using__(opts) do + opts = Keyword.validate!(opts, [:service]) + quote bind_quoted: [opts: opts] do service_mod = opts[:service] service_name = service_mod.__meta__(:name) - Enum.each(service_mod.__rpc_calls__(), fn {name, {_, req_stream}, {_, res_stream}} = rpc -> + Enum.each(service_mod.__rpc_calls__(), fn {name, {_, req_stream}, {_, res_stream}, _options} = + rpc -> func_name = name |> to_string |> Macro.underscore() path = "/#{service_name}/#{name}" grpc_type = GRPC.Service.grpc_type(rpc) @@ -74,7 +80,7 @@ defmodule GRPC.Stub do } if req_stream do - def unquote(String.to_atom(func_name))(channel, opts \\ []) do + def unquote(String.to_atom(func_name))(%GRPC.Channel{} = channel, opts \\ []) do GRPC.Stub.call( unquote(service_mod), unquote(Macro.escape(rpc)), @@ -84,7 +90,7 @@ defmodule GRPC.Stub do ) end else - def unquote(String.to_atom(func_name))(channel, request, opts \\ []) do + def unquote(String.to_atom(func_name))(%GRPC.Channel{} = channel, request, opts \\ []) do GRPC.Stub.call( unquote(service_mod), unquote(Macro.escape(rpc)), @@ -121,42 +127,100 @@ defmodule GRPC.Stub do * `:interceptors` - client interceptors * `:codec` - client will use this to encode and decode binary message * `:compressor` - the client will use this to compress requests and decompress responses. If this is set, accepted_compressors - will be appended also, so this can be used safely without `:accesspted_compressors`. + will be appended also, so this can be used safely without `:accepted_compressors`. * `:accepted_compressors` - tell servers accepted compressors, this can be used without `:compressor` * `:headers` - headers to attach to each request """ - @spec connect(String.t(), Keyword.t()) :: {:ok, GRPC.Channel.t()} | {:error, any} + @spec connect(String.t(), keyword()) :: {:ok, Channel.t()} | {:error, any()} def connect(addr, opts \\ []) when is_binary(addr) and is_list(opts) do - {host, port} = - case String.split(addr, ":") do - [host, port] -> {host, port} - [socket_path] -> {{:local, socket_path}, 0} - end + # This works because we only accept `http` and `https` schemes (allowlisted below explicitly) + # addresses like "localhost:1234" parse as if `localhost` is the scheme for URI, and this falls through to + # the base case. Accepting only `http/https` is a trait of `connect/3`. + + case URI.parse(addr) do + %URI{scheme: @secure_scheme, host: host, port: port} -> + opts = Keyword.put_new_lazy(opts, :cred, &default_ssl_option/0) + connect(host, port, opts) + + %URI{scheme: @insecure_scheme, host: host, port: port} -> + if opts[:cred] do + raise ArgumentError, "invalid option for insecure (http) address: :cred" + end + + connect(host, port, opts) + + # For compatibility with previous versions, we accept URIs in + # the "#{address}:#{port}" format + _ -> + case String.split(addr, ":") do + [socket_path] -> + connect({:local, socket_path}, 0, opts) - connect(host, port, opts) + [address, port] -> + port = String.to_integer(port) + connect(address, port, opts) + end + end end - @spec connect(String.t(), binary | non_neg_integer, keyword) :: - {:ok, Channel.t()} | {:error, any} + if {:module, CAStore} == Code.ensure_loaded(CAStore) do + defp default_ssl_option do + %GRPC.Credential{ + ssl: [ + verify: :verify_peer, + depth: 99, + cacert_file: CAStore.file_path() + ] + } + end + else + defp default_ssl_option do + raise """ + no GRPC credentials provided. Please either: + + - Pass the `:cred` option to `GRPC.Stub.connect/2,3` + - Add `:castore` to your list of dependencies in `mix.exs` + """ + end + end + + @spec connect( + String.t() | {:local, String.t()}, + binary() | non_neg_integer(), + keyword() + ) :: {:ok, Channel.t()} | {:error, any()} def connect(host, port, opts) when is_binary(port) do connect(host, String.to_integer(port), opts) end def connect(host, port, opts) when is_integer(port) do - adapter = - Keyword.get( - opts, - :adapter, - Application.get_env(:grpc, :http2_client_adapter, GRPC.Adapter.Gun) + if Application.get_env(:grpc, :http2_client_adapter) do + raise "the :http2_client_adapter config key has been deprecated.\ + The currently supported way is to configure it\ + through the :adapter option for GRPC.Stub.connect/3" + end + + opts = + Keyword.validate!(opts, + cred: nil, + adapter: GRPC.Client.Adapters.Gun, + adapter_opts: [], + interceptors: [], + codec: GRPC.Codec.Proto, + compressor: nil, + accepted_compressors: [], + headers: [] ) - cred = Keyword.get(opts, :cred) + adapter = opts[:adapter] + + cred = opts[:cred] scheme = if cred, do: @secure_scheme, else: @insecure_scheme - interceptors = Keyword.get(opts, :interceptors, []) |> init_interceptors - codec = Keyword.get(opts, :codec, GRPC.Codec.Proto) - compressor = Keyword.get(opts, :compressor) - accepted_compressors = Keyword.get(opts, :accepted_compressors) || [] - headers = Keyword.get(opts, :headers) || [] + interceptors = init_interceptors(opts[:interceptors]) + codec = opts[:codec] + compressor = opts[:compressor] + accepted_compressors = opts[:accepted_compressors] + headers = opts[:headers] accepted_compressors = if compressor do @@ -165,6 +229,12 @@ defmodule GRPC.Stub do accepted_compressors end + adapter_opts = opts[:adapter_opts] + + unless is_list(adapter_opts) do + raise ArgumentError, ":adapter_opts must be a keyword list if present" + end + %Channel{ host: host, port: port, @@ -177,7 +247,7 @@ defmodule GRPC.Stub do accepted_compressors: accepted_compressors, headers: headers } - |> adapter.connect(opts[:adapter_opts]) + |> adapter.connect(adapter_opts) end def retry_timeout(curr) when curr < 11 do @@ -188,14 +258,7 @@ defmodule GRPC.Stub do 120_000 end - uniform_fn = - if function_exported?(:rand, :uniform_real, 0) do - :uniform_real - else - :uniform - end - - jitter = (apply(:rand, uniform_fn, []) - 0.5) / 2.5 + jitter = (:rand.uniform_real() - 0.5) / 2.5 round(timeout + jitter * timeout) end @@ -210,33 +273,31 @@ defmodule GRPC.Stub do @doc """ Disconnects the adapter and frees any resources the adapter is consuming """ - @spec disconnect(Channel.t()) :: {:ok, Channel.t()} | {:error, any} + @spec disconnect(Channel.t()) :: {:ok, Channel.t()} | {:error, any()} def disconnect(%Channel{adapter: adapter} = channel) do adapter.disconnect(channel) end - @doc """ - The actual function invoked when invoking a rpc function. - - ## Returns - - * Unary calls. `{:ok, reply} | {:ok, headers_map} | {:error, error}` - * Client streaming. A `GRPC.Client.Stream` - * Server streaming. `{:ok, Enumerable.t} | {:ok, Enumerable.t, trailers_map} | {:error, error}` - - ## Options - - * `:timeout` - request timeout. Default is 10s for unary calls and `:infinity` for - client or server streaming calls - * `:deadline` - when the request is timeout, will override timeout - * `:metadata` - a map, your custom metadata - * `:return_headers` - default is false. When it's true, a three elem tuple will be returned - with the last elem being a map of headers `%{headers: headers, trailers: trailers}`(unary) or - `%{headers: headers}`(server streaming) - """ - @spec call(atom, tuple, GRPC.Client.Stream.t(), struct | nil, keyword) :: rpc_return + @doc false + # # The actual function invoked when invoking an RPC function. + # + # Returns + # + # * Unary calls. `{:ok, reply} | {:ok, headers_map} | {:error, error}` + # * Client streaming. A `GRPC.Client.Stream` + # * Server streaming. `{:ok, Enumerable.t} | {:ok, Enumerable.t, trailers_map} | {:error, error}` + # + # Options + # + # * `:timeout` - request timeout. Default is 10s for unary calls and `:infinity` for + # client or server streaming calls + # * `:deadline` - when the request is timeout, will override timeout + # * `:metadata` - a map, your custom metadata + # * `:return_headers` - default is false. When it's true, a three elem tuple will be returned + # with the last elem being a map of headers `%{headers: headers, trailers: trailers}`(unary) or + # `%{headers: headers}`(server streaming) def call(_service_mod, rpc, %{channel: channel} = stream, request, opts) do - {_, {req_mod, req_stream}, {res_mod, response_stream}} = rpc + {_, {req_mod, req_stream}, {res_mod, response_stream}, _rpc_options} = rpc stream = %{stream | request_mod: req_mod, response_mod: res_mod} @@ -247,8 +308,8 @@ defmodule GRPC.Stub do parse_req_opts([{:timeout, @default_timeout} | opts]) end - compressor = Map.get(opts, :compressor, channel.compressor) - accepted_compressors = Map.get(opts, :accepted_compressors, []) + compressor = Keyword.get(opts, :compressor, channel.compressor) + accepted_compressors = Keyword.get(opts, :accepted_compressors, []) accepted_compressors = if compressor do @@ -259,12 +320,14 @@ defmodule GRPC.Stub do stream = %{ stream - | codec: Map.get(opts, :codec, channel.codec), - compressor: Map.get(opts, :compressor, channel.compressor), + | codec: Keyword.get(opts, :codec, channel.codec), + compressor: Keyword.get(opts, :compressor, channel.compressor), accepted_compressors: accepted_compressors } - do_call(req_stream, stream, request, opts) + GRPC.Telemetry.client_span(stream, request, fn -> + do_call(req_stream, stream, request, opts) + end) end defp do_call( @@ -275,7 +338,7 @@ defmodule GRPC.Stub do ) do last = fn %{codec: codec, compressor: compressor} = s, _ -> message = codec.encode(request) - opts = Map.put(opts, :compressor, compressor) + opts = Keyword.put(opts, :compressor, compressor) s |> channel.adapter.send_request(message, opts) @@ -303,14 +366,6 @@ defmodule GRPC.Stub do next.(stream, req) end - @doc """ - DEPRECATED. Use `send_request/3` instead - """ - @deprecated "Use send_request/3 instead" - def stream_send(stream, request, opts \\ []) do - send_request(stream, request, opts) - end - @doc """ Send streaming requests. @@ -322,7 +377,7 @@ defmodule GRPC.Stub do * `:end_stream` - indicates it's the last one request, then the stream will be in half_closed state. Default is false. """ - @spec send_request(GRPC.Client.Stream.t(), struct, Keyword.t()) :: GRPC.Client.Stream.t() + @spec send_request(GRPC.Client.Stream.t(), struct, keyword()) :: GRPC.Client.Stream.t() def send_request(%{__interface__: interface} = stream, request, opts \\ []) do interface[:send_request].(stream, request, opts) end @@ -347,8 +402,8 @@ defmodule GRPC.Stub do After that, callings to `recv/2` will return a CANCEL error. """ - def cancel(%{channel: channel, payload: payload} = stream) do - case channel.adapter.cancel(channel.adapter_payload, payload) do + def cancel(%{channel: channel} = stream) do + case channel.adapter.cancel(stream) do :ok -> %{stream | canceled: true} other -> other end @@ -370,21 +425,39 @@ defmodule GRPC.Stub do {:ok, reply} = GRPC.Stub.recv(stream) # Reply is streaming - {:ok, enum} = GRPC.Stub.recv(stream) - replies = Enum.map(enum, fn({:ok, reply}) -> reply end) + {:ok, ex_stream} = GRPC.Stub.recv(stream) + replies = Enum.map(ex_stream, fn({:ok, reply}) -> reply end) ## Options * `:timeout` - request timeout * `:deadline` - when the request is timeout, will override timeout * `:return_headers` - when true, headers will be returned. + + ## Stream behavior + We build the Stream struct using `Stream.unfold/2`. + + The unfold function is built in such a way that - for both adapters - the accumulator is a map used to find the + `connection_stream`process and the `next_fun` argument is a function that reads directly from the `connection_stream` + that is producing data. + Every time we execute `next_fun` we read a chunk of data. This means that `next_fun` will have the side effect of updating the state of the `connection_stream` process, removing the chunk of data that's being read from the underlying `GenServer`'s state. + + + ## Examples + + iex> ex_stream |> Stream.take(1) |> Enum.to_list() + [1] + iex> ex_stream |> Enum.to_list() + [2, 3] + iex> ex_stream |> Enum.to_list() + [] """ - @spec recv(GRPC.Client.Stream.t(), keyword | map) :: - {:ok, struct} - | {:ok, struct, map} + @spec recv(GRPC.Client.Stream.t(), keyword()) :: + {:ok, struct()} + | {:ok, struct(), map()} | {:ok, Enumerable.t()} - | {:ok, Enumerable.t(), map} - | {:error, any} + | {:ok, Enumerable.t(), map()} + | {:error, any()} def recv(stream, opts \\ []) def recv(%{canceled: true}, _) do @@ -394,289 +467,56 @@ defmodule GRPC.Stub do def recv(%{__interface__: interface} = stream, opts) do opts = if is_list(opts) do - parse_recv_opts(opts) + parse_recv_opts(Keyword.put_new(opts, :timeout, @default_timeout)) else opts end - interface[:recv].(stream, opts) - end - - @doc false - def do_recv(%{server_stream: true, channel: channel, payload: payload} = stream, opts) do - case recv_headers(channel.adapter, channel.adapter_payload, payload, opts) do - {:ok, headers, is_fin} -> - res_enum = - case is_fin do - :fin -> [] - :nofin -> response_stream(stream, opts) - end - - if opts[:return_headers] do - {:ok, res_enum, %{headers: headers}} - else - {:ok, res_enum} - end - - {:error, reason, headers} -> - if opts[:return_headers] do - {:error, reason, %{headers: headers}} - else - {:error, reason} - end - end - end - - def do_recv( - %{payload: payload, channel: channel} = stream, - opts - ) do - with {:ok, headers, _is_fin} <- - recv_headers(channel.adapter, channel.adapter_payload, payload, opts), - {:ok, body, trailers} <- - recv_body(channel.adapter, channel.adapter_payload, payload, opts) do - {status, msg} = parse_response(stream, headers, body, trailers) - - if opts[:return_headers] do - {status, msg, %{headers: headers, trailers: trailers}} - else - {status, msg} - end - else - error = {:error, reason, _} -> - if opts[:return_headers] do - error - else - {:error, reason} - end - end - end - - defp recv_headers(adapter, conn_payload, stream_payload, opts) do - case adapter.recv_headers(conn_payload, stream_payload, opts) do - {:ok, headers, is_fin} -> - {:ok, GRPC.Transport.HTTP2.decode_headers(headers), is_fin} - - other -> - other - end - end - - defp recv_body(adapter, conn_payload, stream_payload, opts) do - recv_body(adapter, conn_payload, stream_payload, "", opts) - end - - defp recv_body(adapter, conn_payload, stream_payload, acc, opts) do - case adapter.recv_data_or_trailers(conn_payload, stream_payload, opts) do - {:data, data} -> - recv_body(adapter, conn_payload, stream_payload, <>, opts) - - {:trailers, trailers} -> - {:ok, acc, GRPC.Transport.HTTP2.decode_headers(trailers)} - - other -> - other - end - end - - defp parse_response( - %{response_mod: res_mod, codec: codec, accepted_compressors: accepted_compressors}, - headers, - body, - trailers - ) do - case parse_trailers(trailers) do - :ok -> - compressor = - case headers do - %{"grpc-encoding" => encoding} -> - Enum.find(accepted_compressors, nil, fn c -> c.name() == encoding end) - - _ -> - nil - end - - case GRPC.Message.from_data(%{compressor: compressor}, body) do - {:ok, msg} -> - {:ok, codec.decode(msg, res_mod)} - - err -> - err - end - - error -> - error - end - end - - defp parse_trailers(trailers) do - status = String.to_integer(trailers["grpc-status"]) - - if status == GRPC.Status.ok() do - :ok - else - {:error, - %GRPC.RPCError{ - status: status, - message: trailers["grpc-message"], - details: decode_status_details(trailers) - }} - end - end - - defp decode_status_details(%{"grpc-status-details-bin" => details}) - when is_binary(details) do - GRPC.Transport.Utils.decode_status_details(details) - end - - defp decode_status_details(_headers), do: nil - - defp response_stream( - %{ - channel: %{adapter: adapter, adapter_payload: ap}, - response_mod: res_mod, - codec: codec, - payload: payload - }, - opts - ) do - state = %{ - adapter: adapter, - adapter_payload: ap, - payload: payload, - buffer: <<>>, - fin: false, - need_more: true, - opts: opts, - response_mod: res_mod, - codec: codec - } - - Stream.unfold(state, fn s -> read_stream(s) end) - end - - defp read_stream(%{buffer: <<>>, fin: true, fin_resp: nil}), do: nil - - defp read_stream(%{buffer: <<>>, fin: true, fin_resp: fin_resp} = s), - do: {fin_resp, Map.put(s, :fin_resp, nil)} - - defp read_stream( - %{ - adapter: adapter, - adapter_payload: ap, - payload: payload, - buffer: buffer, - need_more: true, - opts: opts - } = s - ) do - case adapter.recv_data_or_trailers(ap, payload, opts) do - {:data, data} -> - buffer = buffer <> data - new_s = s |> Map.put(:need_more, false) |> Map.put(:buffer, buffer) - read_stream(new_s) - - {:trailers, trailers} -> - trailers = GRPC.Transport.HTTP2.decode_headers(trailers) - - case parse_trailers(trailers) do - :ok -> - fin_resp = - if opts[:return_headers] do - {:trailers, trailers} - end - - new_s = s |> Map.put(:fin, true) |> Map.put(:fin_resp, fin_resp) - read_stream(new_s) - - error -> - {error, %{buffer: <<>>, fin: true, fin_resp: nil}} - end - - error = {:error, _, _} -> - {error, %{buffer: <<>>, fin: true, fin_resp: nil}} - end - end - - defp read_stream(%{buffer: buffer, need_more: false, response_mod: res_mod, codec: codec} = s) do - case GRPC.Message.get_message(buffer) do - # TODO - {{_, message}, rest} -> - reply = codec.decode(message, res_mod) - new_s = Map.put(s, :buffer, rest) - {{:ok, reply}, new_s} - - _ -> - read_stream(Map.put(s, :need_more, true)) - end - end - - defp parse_req_opts(list) when is_list(list) do - parse_req_opts(list, %{}) - end - - defp parse_req_opts([{:timeout, timeout} | t], acc) do - parse_req_opts(t, Map.put(acc, :timeout, timeout)) - end - - defp parse_req_opts([{:deadline, deadline} | t], acc) do - parse_req_opts(t, Map.put(acc, :timeout, GRPC.TimeUtils.to_relative(deadline))) - end - - defp parse_req_opts([{:compressor, compressor} | t], acc) do - parse_req_opts(t, Map.put(acc, :compressor, compressor)) - end - - defp parse_req_opts([{:accepted_compressors, compressors} | t], acc) do - parse_req_opts(t, Map.put(acc, :accepted_compressors, compressors)) - end - - defp parse_req_opts([{:grpc_encoding, grpc_encoding} | t], acc) do - parse_req_opts(t, Map.put(acc, :grpc_encoding, grpc_encoding)) - end - - defp parse_req_opts([{:metadata, metadata} | t], acc) do - parse_req_opts(t, Map.put(acc, :metadata, metadata)) - end - - defp parse_req_opts([{:content_type, content_type} | t], acc) do - Logger.warn(":content_type has been deprecated, please use :codec") - parse_req_opts(t, Map.put(acc, :content_type, content_type)) - end - - defp parse_req_opts([{:codec, codec} | t], acc) do - parse_req_opts(t, Map.put(acc, :codec, codec)) - end - - defp parse_req_opts([{:return_headers, return_headers} | t], acc) do - parse_req_opts(t, Map.put(acc, :return_headers, return_headers)) - end - - defp parse_req_opts([{key, _} | _], _) do - raise ArgumentError, "option #{inspect(key)} is not supported" + interface[:receive_data].(stream, opts) + end + + @valid_req_opts [ + :timeout, + :deadline, + :compressor, + :accepted_compressors, + :grpc_encoding, + :metadata, + :codec, + :return_headers + ] + defp parse_req_opts(opts) when is_list(opts) do + # Map.new is used so we can keep the last value + # passed for a given key + opts + |> Map.new(fn + {:deadline, deadline} -> + {:timeout, GRPC.TimeUtils.to_relative(deadline)} + + {key, value} when key in @valid_req_opts -> + {key, value} + + {key, _} -> + raise ArgumentError, "option #{inspect(key)} is not supported" + end) + |> Map.to_list() end - defp parse_req_opts(_, acc), do: acc - defp parse_recv_opts(list) when is_list(list) do - parse_recv_opts(list, %{timeout: @default_timeout}) - end - - defp parse_recv_opts([{:timeout, timeout} | t], acc) do - parse_recv_opts(t, Map.put(acc, :timeout, timeout)) - end + # Map.new is used so we can keep the last value + # passed for a given key - defp parse_recv_opts([{:deadline, deadline} | t], acc) do - parse_recv_opts(t, Map.put(acc, :deadline, GRPC.TimeUtils.to_relative(deadline))) - end + list + |> Map.new(fn + {:deadline, deadline} -> + {:deadline, GRPC.TimeUtils.to_relative(deadline)} - defp parse_recv_opts([{:return_headers, return_headers} | t], acc) do - parse_recv_opts(t, Map.put(acc, :return_headers, return_headers)) - end + {key, _} when key not in @valid_req_opts -> + raise ArgumentError, "option #{inspect(key)} is not supported" - defp parse_recv_opts([{key, _} | _], _) do - raise ArgumentError, "option #{inspect(key)} is not supported" + kv -> + kv + end) + |> Map.to_list() end - - defp parse_recv_opts(_, acc), do: acc end diff --git a/lib/grpc/telemetry.ex b/lib/grpc/telemetry.ex new file mode 100644 index 000000000..8764493dc --- /dev/null +++ b/lib/grpc/telemetry.ex @@ -0,0 +1,163 @@ +defmodule GRPC.Telemetry do + @moduledoc """ + Events published by GRPC + + These can be divided in client-side events and server-side events. + + ## Client-side Events + + * `[:grpc, :client, :rpc, :start]` - Published before all interceptors are executed. + * `[:grpc, :client, :rpc, :stop]` - Published after all interceptors executed successfully. + * `:duration` - the duration as measured through `System.monotonic_time()` + for the whole interceptor pipeline. + * `[:grpc, :client, :rpc, :exception]` - Published if any exception occurs while receiving a message. + * `:duration` - the duration as measured through `System.monotonic_time()` + for the execution since the start of the pipeline until the exception happened. + + | event | measurements | metadata | + |--------------|--------------|----------| + | `[:rpc, :start]` | `:count` | `:stream` | + | `[:rpc, :stop]` | `:duration` | `:stream` | + | `[:rpc, :exception]` | `:duration` | `:stream`, `:kind`, `:reason`, `:stacktrace` | + + ### Metadata + + * `:stream` - the `%GRPC.Server.Stream{}` for the request + * `:function_name` - the name of the function called + * `:server` - the server module name + * `:endpoint` - the endpoint module name + * `:request` - the client request + * `:result` - the result returned from the interceptor pipeline. + + `:exception` events also include some error metadata: + + * `:reason` is the error value in case of `catch` or the actual exception in case of `rescue`. + * `:kind` can be one of: + * `:error` — from an `{:error, error}` return value. Some Erlang functions may also throw an + `:error` tuple, which will be reported as `:error`. + * `:exit` — from a caught process exit. + * `:throw` — from a caught value, this doesn't necessarily mean that an error occurred. + + ## Server-side Events + + * `[:grpc, :server, :rpc, :start]` - Published before all interceptors are executed. + * `[:grpc, :server, :rpc, :stop]` - Published after all interceptors executed successfully. + * `:duration` - the duration as measured through `System.monotonic_time()` + for the whole interceptor pipeline. + * `[:grpc, :server, :rpc, :exception]` - Published if any exception occurs while receiving a message. + * `:duration` - the duration as measured through `System.monotonic_time()` + for the execution since the start of the pipeline until the exception happened. + + | event | measurements | metadata | + |--------------|--------------|----------| + | `[:rpc, :start]` | `:count` | `:stream`, `:server`, `:endpoint`, `:function_name` | + | `[:rpc, :stop]` | `:duration` | `:stream`, `:server`, `:endpoint`, `:function_name` , `:result` | + | `[:rpc, :exception]` | `:duration` | `:stream`, `:server`, `:endpoint`, `:function_name`, `:kind`, `:reason`, `:stacktrace` | + + ### Metadata + + * `:stream` - the `%GRPC.Server.Stream{}` for the request. + * `:function_name` - the name of the function called. + * `:server` - the server module name. + * `:endpoint` - the endpoint module name. + * `:result` - the result returned from the interceptor pipeline. + + `:exception` events also include some error metadata: + + * `:reason` is the error value in case of `catch` or the actual exception in case of `rescue`. + * `:kind` can be one of: + * `:error` — from an `{:error, error}` return value. Some Erlang functions may also throw an + `:error` tuple, which will be reported as `:error`. + * `:exit` — from a caught process exit. + * `:throw` — from a caught value, this doesn't necessarily mean that an error occurred. + """ + + require Logger + + @server_rpc [:grpc, :server, :rpc] + @client_rpc [:grpc, :client, :rpc] + + @doc "The server telemetry event prefix." + def server_rpc_prefix, do: @server_rpc + + @doc "The client telemetry event prefix." + def client_rpc_prefix, do: @client_rpc + + @doc false + def server_span(server, endpoint, func_name, stream, span_fn) do + start_metadata = %{ + server: server, + endpoint: endpoint, + function_name: func_name, + stream: stream + } + + :telemetry.span(@server_rpc, start_metadata, fn -> + result = span_fn.() + + {result, Map.put(start_metadata, :result, result)} + end) + rescue + e in GRPC.RPCError -> + {:error, e} + catch + kind, reason -> + stacktrace = __STACKTRACE__ + Logger.error(Exception.format(kind, reason, stacktrace)) + {:error, %{kind: kind, reason: reason, stack: stacktrace}} + end + + @client_rpc_start_name @client_rpc ++ [:start] + @doc false + def client_rpc_start_name, do: @client_rpc_start_name + + @doc false + def client_span(stream, request, span_fn) do + start_metadata = %{stream: stream, request: request} + + :telemetry.span(@client_rpc, start_metadata, fn -> + try do + result = span_fn.() + + {result, Map.put(start_metadata, :result, result)} + rescue + e -> + :erlang.error(Exception.normalize(:error, e, __STACKTRACE__)) + end + end) + catch + kind, reason -> + stacktrace = __STACKTRACE__ + Logger.error(Exception.format(kind, reason, stacktrace)) + :erlang.raise(kind, reason, stacktrace) + end + + @client_rpc_stop_name @client_rpc ++ [:stop] + @doc false + def client_rpc_stop_name, do: @client_rpc_stop_name + + @doc false + def client_rpc_stop(stream, duration) do + :telemetry.execute(@client_rpc_stop_name, %{duration: duration}, %{stream: stream}) + end + + @client_rpc_exception_name @client_rpc ++ [:exception] + @doc false + def client_rpc_exception_name, do: @client_rpc_exception_name + + @doc false + def client_rpc_exception( + stream, + kind, + reason, + stacktrace, + duration + ) do + :telemetry.execute(@client_rpc_exception_name, %{duration: duration}, %{ + stream: stream, + kind: kind, + reason: reason, + stacktrace: stacktrace + }) + end +end diff --git a/lib/grpc/transport/http2.ex b/lib/grpc/transport/http2.ex index a87d0db19..0d47631f5 100644 --- a/lib/grpc/transport/http2.ex +++ b/lib/grpc/transport/http2.ex @@ -8,30 +8,32 @@ defmodule GRPC.Transport.HTTP2 do require Logger + def server_headers(%{codec: GRPC.Codec.WebText = codec}) do + %{"content-type" => "application/grpc-web-#{codec.name()}"} + end + + # TO-DO: refactor when we add a GRPC.Codec.content_type callback + def server_headers(%{codec: GRPC.Codec.JSON}) do + %{"content-type" => "application/json"} + end + def server_headers(%{codec: codec}) do %{"content-type" => "application/grpc+#{codec.name()}"} end - @spec server_trailers(integer, String.t(), String.t()) :: map - def server_trailers(status \\ Status.ok(), message \\ "", status_details \\ "") do - {_, status_details} = encode_metadata_pair({"grpc-status-details-bin", status_details}) - - trailers = %{ + @spec server_trailers(integer, String.t()) :: map + def server_trailers(status \\ Status.ok(), message \\ "") do + %{ "grpc-status" => Integer.to_string(status), - "grpc-message" => message + "grpc-message" => URI.encode(message) } - - case status_details do - "" -> trailers - status_details -> Map.put(trailers, "grpc-status-details-bin", status_details) - end end @doc """ Now we may not need this because gun already handles the pseudo headers. """ - @spec client_headers(GRPC.Client.Stream.t(), map) :: [{String.t(), String.t()}] - def client_headers(%{channel: channel, path: path} = s, opts \\ %{}) do + @spec client_headers(GRPC.Client.Stream.t(), keyword()) :: [{String.t(), String.t()}] + def client_headers(%{channel: channel, path: path} = s, opts \\ []) do [ {":method", "POST"}, {":scheme", channel.scheme}, @@ -40,10 +42,12 @@ defmodule GRPC.Transport.HTTP2 do ] ++ client_headers_without_reserved(s, opts) end - @spec client_headers_without_reserved(GRPC.Client.Stream.t(), map) :: [{String.t(), String.t()}] - def client_headers_without_reserved(%{codec: codec} = stream, opts \\ %{}) do + @spec client_headers_without_reserved(GRPC.Client.Stream.t(), keyword()) :: [ + {String.t(), String.t()} + ] + def client_headers_without_reserved(%{codec: codec} = stream, opts \\ []) do [ - # It seems only gRPC implementations only support "application/grpc", so we support :content_type now. + # It seems only gRPC implemenations only support "application/grpc", so we support :content_type now. {"content-type", content_type(opts[:content_type], codec)}, {"user-agent", "grpc-elixir/#{opts[:grpc_version] || GRPC.version()}"}, {"te", "trailers"} @@ -62,15 +66,11 @@ defmodule GRPC.Transport.HTTP2 do defp content_type(custom, _codec) when is_binary(custom), do: custom - defp content_type(_, codec) do - # Some gRPC implementations don't support application/grpc+xyz, - # to avoid this kind of trouble, use application/grpc by default - if codec == GRPC.Codec.Proto do - "application/grpc" - else - "application/grpc+#{codec.name()}" - end - end + # Some gRPC implementations don't support application/grpc+xyz, + # to avoid this kind of trouble, use application/grpc by default + defp content_type(_, GRPC.Codec.Proto), do: "application/grpc" + defp content_type(_, codec = GRPC.Codec.WebText), do: "application/grpc-web-#{codec.name()}" + defp content_type(_, codec), do: "application/grpc+#{codec.name()}" def extract_metadata(headers) do headers @@ -83,7 +83,7 @@ defmodule GRPC.Transport.HTTP2 do if is_metadata(k) do decode_metadata({k, v}) else - {k, v} + decode_reserved({k, v}) end end) end @@ -98,7 +98,7 @@ defmodule GRPC.Transport.HTTP2 do end defp append_encoding(headers, grpc_encoding) when is_binary(grpc_encoding) do - Logger.warn("grpc_encoding option is deprecated, please use compressor.") + Logger.warning("grpc_encoding option is deprecated, please use compressor.") [{"grpc-encoding", grpc_encoding} | headers] end @@ -138,26 +138,22 @@ defmodule GRPC.Transport.HTTP2 do end defp encode_metadata_pair({key, val}) do - # Implementations ... should emit un-padded values - val = if String.ends_with?(key, "-bin"), do: Base.encode64(val, padding: false), else: val - {String.downcase(key), val} + val = if String.ends_with?(key, "-bin"), do: Base.encode64(val), else: val + {String.downcase(to_string(key)), val} end - defp decode_metadata(kv = {key, val}) do - # Implementations MUST accept padded and un-padded values - if String.ends_with?(key, "-bin") do - if rem(IO.iodata_length(val), 4) == 0 do - {key, Base.decode64!(val)} - else - {key, Base.decode64!(val, padding: false)} - end - else - kv - end + defp decode_metadata({key, val}) do + val = if String.ends_with?(key, "-bin"), do: Base.decode64!(val, padding: false), else: val + {key, val} end + defp decode_reserved({"grpc-message" = key, val}) do + {key, URI.decode(val)} + end + + defp decode_reserved(kv), do: kv + defp is_reserved_header(":" <> _), do: true - defp is_reserved_header("grpc-status-details-bin"), do: false defp is_reserved_header("grpc-" <> _), do: true defp is_reserved_header("content-type"), do: true defp is_reserved_header("te"), do: true diff --git a/lib/grpc/transport/utils.ex b/lib/grpc/transport/utils.ex index c29cc052b..6817b3dc2 100644 --- a/lib/grpc/transport/utils.ex +++ b/lib/grpc/transport/utils.ex @@ -82,7 +82,7 @@ defmodule GRPC.Transport.Utils do def encode_status_details(_errorcode, nil), do: "" def encode_status_details(errorcode, details) do - Google.Rpc.Status.new(code: errorcode, details: Enum.map(details, &build_any/1)) + %Google.Rpc.Status{code: errorcode, details: Enum.map(details, &build_any/1)} |> Google.Rpc.Status.encode() end @@ -99,7 +99,7 @@ defmodule GRPC.Transport.Utils do end defp build_any(message = %{__struct__: type}) do - Google.Protobuf.Any.new(type_url: get_type_url(type), value: Protobuf.encode(message)) + %Google.Protobuf.Any{type_url: get_type_url(type), value: Protobuf.encode(message)} end def get_type_url(type) do diff --git a/lib/mix/tasks/grpc.server.ex b/lib/mix/tasks/grpc.server.ex deleted file mode 100644 index d0e856ed3..000000000 --- a/lib/mix/tasks/grpc.server.ex +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Mix.Tasks.Grpc.Server do - use Mix.Task - - @shortdoc "Starts applications and their servers" - - @moduledoc """ - Starts the application by configuring `start_server` to true. - - The `--no-halt` flag is automatically added. - """ - @impl true - def run(args) do - Application.put_env(:grpc, :start_server, true, persistent: true) - Mix.Task.run("run", run_args() ++ args) - end - - defp run_args do - if iex_running?(), do: [], else: ["--no-halt"] - end - - defp iex_running? do - Code.ensure_loaded?(IEx) and IEx.started?() - end -end diff --git a/livebooks/telemetry.livemd b/livebooks/telemetry.livemd new file mode 100644 index 000000000..deaf0c04a --- /dev/null +++ b/livebooks/telemetry.livemd @@ -0,0 +1,337 @@ +# Telemetry + +```elixir +my_app_root = Path.join(__DIR__, "..") + +Mix.install( + [ + {:grpc, path: my_app_root, env: :dev}, + {:telemetry_metrics, "~> 0.7"}, + {:telemetry_metrics_prometheus, "~> 1.1"}, + {:req, "~> 0.3"} + ], + config_path: Path.join(my_app_root, "config/config.exs"), + lockfile: Path.join(my_app_root, "mix.lock") +) +``` + +## Telemetry Events + +We know from the documentation `GRPC.Telemetry` that some server-side events and some client-side events are published. We can use those events to build a Prometheus metrics export through `Telemetry.Metrics` and [`TelemetryMetricsPrometheus`](https://hexdocs.pm/telemetry_metrics_prometheus/TelemetryMetricsPrometheus.html) that's retrocompatible to the deprecated `:grpc_prometheus` library. + +First, let's create a mock server and client that we can use. + +## GRPC Server and Client + +```elixir +# This whole code block is taken from test/support/proto/helloword.pb.ex +# Normally, this would be autogenerated from a valid protobuf file, +# but we're using this hardcoded version so that this Livebook is self-contained. + +defmodule Helloworld.HelloRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field(:name, 1, type: :string) + field(:duration, 2, proto3_optional: true, type: :int32) +end + +defmodule Helloworld.HelloReply do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field(:message, 1, type: :string) +end + +defmodule Helloworld.HeaderRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 +end + +defmodule Helloworld.HeaderReply do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field(:authorization, 1, type: :string) +end + +defmodule Helloworld.Greeter.Service do + @moduledoc false + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.11.0" + + rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply) + + rpc(:CheckHeaders, Helloworld.HeaderRequest, Helloworld.HeaderReply) +end + +defmodule Helloworld.Greeter.Stub do + @moduledoc false + use GRPC.Stub, service: Helloworld.Greeter.Service +end +``` + +```elixir +defmodule HelloServer do + use GRPC.Server, service: Helloworld.Greeter.Service + + def say_hello(%{name: "raise", duration: duration}, _stream) do + Process.sleep(duration) + raise ArgumentError, "exception raised" + end + + def say_hello(%{name: "ok", duration: duration}, _stream) do + Process.sleep(duration) + Helloworld.HelloReply.new(message: "Hello") + end + + def say_hello(%{name: "not_found", duration: duration}, _stream) do + Process.sleep(duration) + raise GRPC.RPCError, status: GRPC.Status.not_found() + end + + def check_headers(_req, stream) do + token = GRPC.Stream.get_headers(stream)["authorization"] + Helloworld.HeaderReply.new(authorization: token) + end +end +``` + +```elixir +defmodule MetricsSupervisor do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + def init(_arg) do + # We attach to these events to combine them into a single published metric + # This can actually be done directly in Prometheus through Recording Rules, + # but this shows how to build a drop-in replacement for :grpc_prometheus + :telemetry.attach_many( + "handler-#{__MODULE__}", + [ + GRPC.Telemetry.server_rpc_prefix() ++ [:stop], + GRPC.Telemetry.server_rpc_prefix() ++ [:exception], + GRPC.Telemetry.client_rpc_prefix() ++ [:stop], + GRPC.Telemetry.client_rpc_prefix() ++ [:exception] + ], + fn [:grpc, server_or_client, :rpc, event_kind], + %{duration: duration}, + %{stream: stream} = metadata, + _opts -> + code = + case {event_kind, metadata[:result], metadata[:reason]} do + {:stop, {:ok, _}, _} -> GRPC.Status.code_name(0) + {:stop, {:ok, _, _}, _} -> GRPC.Status.code_name(0) + {:stop, {:error, %GRPC.RPCError{status: status}}, _} -> GRPC.Status.code_name(status) + {:exception, _, %GRPC.RPCError{status: status}} -> GRPC.Status.code_name(status) + _ -> GRPC.Status.code_name(GRPC.Status.unknown()) + end + + metadata = %{ + grpc_service: stream.service_name, + grpc_method: stream.method_name, + grpc_type: stream.grpc_type, + grpc_code: code + } + + if is_message(stream) do + :telemetry.execute( + [:custom_grpc, :"#{server_or_client}_rpc", :sent], + %{duration: duration}, + metadata + ) + end + + :telemetry.execute( + [:custom_grpc, :"#{server_or_client}_rpc", :handled], + %{duration: duration}, + metadata + ) + end, + nil + ) + + # This can also be achieved through some clever use of tags+tag_values, + # without having to attach and publish a new event. However, that would + # end up leaking an extraneous tag to Prometheus. + # This is cleaner in that sense. + :telemetry.attach_many( + "handler-#{__MODULE__}-start", + [ + GRPC.Telemetry.server_rpc_prefix() ++ [:start], + GRPC.Telemetry.client_rpc_prefix() ++ [:start] + ], + fn _event, _, %{stream: stream}, _opts -> + if is_message(stream) do + :telemetry.execute([:custom_grpc, :server_rpc, :message_received], %{count: 1}) + end + end, + nil + ) + + children = [ + {TelemetryMetricsPrometheus, [metrics: metrics(), port: 9568]} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + defp is_message(stream) do + stream.grpc_type in [:client_stream, :bidirectional_stream] + end + + @histogram_buckets_seconds [5.0e-3, 10.0e-3, 25.0e-3, 50.0e-3, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] + + @tags [:grpc_service, :grpc_method, :grpc_type] + @tags_with_code [:grpc_code | @tags] + + defp metrics do + [ + # Server Metrics + counter( + "grpc_server_started_total", + event_name: "grpc.server.rpc.start", + measurement: :count, + tags: @tags, + tag_values: &extract_tags_from_stream/1, + description: "Total number of RPCs started on the server" + ), + counter( + "grpc_server_msg_received_total", + event_name: "custom_grpc.server_rpc.message_received", + measurement: :count, + tags: @tags, + tag_values: &extract_tags_from_stream/1, + description: "Total number of RPC stream messages received on the server" + ), + counter( + "grpc_server_msg_sent_total", + event_name: "custom_grpc.server_rpc.sent", + measurement: :duration, + tags: @tags, + description: "Total number of gRPC stream messages sent by the server." + ), + counter( + "grpc_server_handled_total", + event_name: "custom_grpc.server_rpc.handled", + measurement: :duration, + tags: @tags_with_code, + description: + "Total number of RPCs completed on the server, regardless of success or failure." + ), + distribution( + "grpc_server_handled_latency_seconds", + event_name: "custom_grpc.server_rpc.handled", + description: "Histogram of response latency of rpcs handled by the server, in seconds.", + measurement: :duration, + tags: @tags_with_code, + unit: {:native, :second}, + reporter_options: [ + buckets: @histogram_buckets_seconds + ] + ), + + # Client Metrics + counter( + "grpc_client_started_total", + event_name: "grpc.client.rpc.start", + measurement: :count, + tags: @tags, + tag_values: &extract_tags_from_stream/1, + description: "Total number of RPCs started on the client" + ), + counter( + "grpc_client_msg_received_total", + event_name: "custom_grpc.client_rpc.message_received", + measurement: :count, + tags: @tags, + tag_values: &extract_tags_from_stream/1, + description: "Total number of RPC stream messages received on the client" + ), + counter( + "grpc_client_msg_sent_total", + event_name: "custom_grpc.client_rpc.sent", + measurement: :duration, + tags: @tags, + description: "Total number of gRPC stream messages sent by the client." + ), + counter( + "grpc_client_handled_total", + event_name: "custom_grpc.client_rpc.handled", + measurement: :duration, + tags: @tags_with_code, + description: + "Total number of RPCs completed on the client, regardless of success or failure." + ), + distribution( + "grpc_client_handled_latency_seconds", + event_name: "custom_grpc.client_rpc.handled", + description: "Histogram of response latency of rpcs handled by the client, in seconds.", + measurement: :duration, + tags: @tags_with_code, + unit: {:native, :second}, + reporter_options: [ + buckets: @histogram_buckets_seconds + ] + ) + ] + end + + defp extract_tags_from_stream(%{ + stream: %{ + service_name: service_name, + method_name: method_name, + grpc_type: grpc_type + } + }) do + %{ + grpc_service: service_name, + grpc_method: method_name, + grpc_type: grpc_type + } + end + + defp extract_tags_from_stream(_) do + %{grpc_service: nil, grpc_method: nil, grpc_type: nil} + end +end +``` + +```elixir +MetricsSupervisor.start_link([]) +GRPC.Server.start([HelloServer], 1337, []) +``` + +```elixir +{:ok, channel} = GRPC.Stub.connect("localhost:1337") + +# the requests take some time internally, so we might not get _exactly_ +# the bucket distribution we expect +for duration <- [0, 6, 101, 101, 501, 1001] do + success_request = Helloworld.HelloRequest.new(name: "ok", duration: duration) + exception_request = Helloworld.HelloRequest.new(name: "raise", duration: duration) + not_found_request = Helloworld.HelloRequest.new(name: "not_found", duration: duration) + + {:ok, _} = Helloworld.Greeter.Stub.say_hello(channel, success_request) + + {:error, %GRPC.RPCError{status: 2}} = + Helloworld.Greeter.Stub.say_hello(channel, exception_request) + + {:error, %GRPC.RPCError{status: 5}} = + Helloworld.Greeter.Stub.say_hello(channel, not_found_request) +end +``` + +```elixir +# Checking which metrics our endpoint returns +# TO-DO: replace this by actual server calls + +{:ok, %Req.Response{status: 200, body: body}} = Req.request(url: "http://localhost:9568/metrics") + +# Print the output in a readable format +IO.puts(body) +nil +``` diff --git a/mix.exs b/mix.exs index 814fc1b13..40e65bc00 100644 --- a/mix.exs +++ b/mix.exs @@ -1,47 +1,52 @@ defmodule GRPC.Mixfile do use Mix.Project + @version "0.10.2" + def project do [ app: :grpc, - version: "0.6.7", - elixir: "~> 1.5", + version: @version, + elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, deps: deps(), package: package(), + aliases: aliases(), description: "The Elixir implementation of gRPC", docs: [ extras: ["README.md"], main: "readme", - source_ref: "v0.6.0", + source_ref: "v#{@version}", source_url: "https://github.com/surgeventures/grpc" ], - dialyzer: [ - plt_add_apps: [:mix, :iex] - ], xref: [exclude: [IEx]] ] end - # Configuration for the OTP application - # - # Type "mix help compile.app" for more information def application do [extra_applications: [:logger]] end + def escript do + [main_module: GRPC.Protoc.CLI, name: "protoc-gen-grpc_elixir"] + end + defp deps do [ - {:protobuf, ">= 0.5.0 and < 0.10.0"}, - {:cowboy, "~> 2.7"}, + {:cowboy, "~> 2.10"}, + {:flow, "~> 1.2"}, {:gun, "~> 2.0"}, - {:ex_doc, "~> 0.23", only: :dev}, - {:inch_ex, "~> 2.0", only: [:dev, :test]}, - {:dialyxir, "~> 1.0", only: [:dev, :test], runtime: false}, - {:junit_formatter, "~> 3.1", only: [:test]}, - {:credo, "~> 1.5", only: [:dev, :test], runtime: false} + {:jason, ">= 0.0.0", optional: true}, + {:cowlib, "~> 2.12"}, + {:castore, "~> 0.1 or ~> 1.0", optional: true}, + {:protobuf, "~> 0.14"}, + {:protobuf_generate, "~> 0.1.1", only: [:dev, :test]}, + {:mint, "~> 1.5"}, + {:ex_doc, "~> 0.29", only: :dev}, + {:ex_parameterized, "~> 1.3.7", only: :test}, + {:telemetry, "~> 1.0"} ] end @@ -50,10 +55,34 @@ defmodule GRPC.Mixfile do name: :grpc_fresha, licenses: ["Apache 2"], links: %{"GitHub" => "https://github.com/surgeventures/grpc"}, - files: ~w(mix.exs README.md lib src config LICENSE .formatter.exs) + files: ~w(mix.exs README.md lib src config priv/templates LICENSE .formatter.exs) } end + defp aliases do + [ + gen_test_protos: &gen_test_protos/1 + ] + end + defp elixirc_paths(:test), do: ["lib", "test/support"] defp elixirc_paths(_), do: ["lib"] + + defp gen_test_protos(_args) do + elixir_out = "test/support" + + cmd = ~s(mix protobuf.generate \ + --include-path=test/support \ + --output-path="#{elixir_out}" \ + --plugin=ProtobufGenerate.Plugins.GRPCWithOptions\ + test/support/transcode_messages.proto \ + test/support/proto/helloworld.proto \ + test/support/proto/route_guide.proto + ) + + case Mix.shell().cmd(cmd) do + 0 -> Mix.Task.rerun("format", [Path.join([elixir_out, "**", "*.pb.ex"])]) + other -> Mix.raise("'protoc' exited with non-zero status: #{other}") + end + end end diff --git a/mix.lock b/mix.lock index 1f431c04b..79d67fe90 100644 --- a/mix.lock +++ b/mix.lock @@ -1,23 +1,22 @@ %{ - "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, + "castore": {:hex, :castore, "1.0.6", "ffc42f110ebfdafab0ea159cd43d31365fa0af0ce4a02ecebf1707ae619ee727", [:mix], [], "hexpm", "374c6e7ca752296be3d6780a6d5b922854ffcc74123da90f2f328996b962d33a"}, + "cowboy": {:hex, :cowboy, "2.11.0", "356bf784599cf6f2cdc6ad12fdcfb8413c2d35dab58404cf000e1feaed3f5645", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "0fa395437f1b0e104e0e00999f39d2ac5f4082ac5049b67a5b6d56ecc31b1403"}, "cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"}, - "credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"}, - "dialyxir": {:hex, :dialyxir, "1.3.0", "fd1672f0922b7648ff9ce7b1b26fcf0ef56dda964a459892ad15f6b4410b5284", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "00b2a4bcd6aa8db9dcb0b38c1225b7277dca9bc370b6438715667071a304696f"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.32", "fa739a0ecfa34493de19426681b23f6814573faee95dfd4b4aafe15a7b5b32c6", [:mix], [], "hexpm", "b8b0dd77d60373e77a3d7e8afa598f325e49e8663a51bcc2b88ef41838cca755"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, - "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, + "ex_doc": {:hex, :ex_doc, "0.31.1", "8a2355ac42b1cc7b2379da9e40243f2670143721dd50748bf6c3b1184dae2089", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.1", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "3178c3a407c557d8343479e1ff117a96fd31bafe52a039079593fb0524ef61b0"}, + "ex_parameterized": {:hex, :ex_parameterized, "1.3.7", "801f85fc4651cb51f11b9835864c6ed8c5e5d79b1253506b5bb5421e8ab2f050", [:mix], [], "hexpm", "1fb0dc4aa9e8c12ae23806d03bcd64a5a0fc9cd3f4c5602ba72561c9b54a625c"}, + "flow": {:hex, :flow, "1.2.4", "1dd58918287eb286656008777cb32714b5123d3855956f29aa141ebae456922d", [:mix], [{:gen_stage, "~> 1.0", [hex: :gen_stage, repo: "hexpm", optional: false]}], "hexpm", "874adde96368e71870f3510b91e35bc31652291858c86c0e75359cbdd35eb211"}, + "gen_stage": {:hex, :gen_stage, "1.2.1", "19d8b5e9a5996d813b8245338a28246307fd8b9c99d1237de199d21efc4c76a1", [:mix], [], "hexpm", "83e8be657fa05b992ffa6ac1e3af6d57aa50aace8f691fcf696ff02f8335b001"}, "gun": {:hex, :gun, "2.0.1", "160a9a5394800fcba41bc7e6d421295cf9a7894c2252c0678244948e3336ad73", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "a10bc8d6096b9502205022334f719cc9a08d9adcfbfc0dbee9ef31b56274a20b"}, - "inch_ex": {:hex, :inch_ex, "2.0.0", "24268a9284a1751f2ceda569cd978e1fa394c977c45c331bb52a405de544f4de", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "96d0ec5ecac8cf63142d02f16b7ab7152cf0f0f1a185a80161b758383c9399a8"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, - "junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"}, - "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, + "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.1", "cc9e3ca312f1cfeccc572b37a09980287e243648108384b97ff2b76e505c3555", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e127a341ad1b209bd80f7bd1620a15693a9908ed780c3b763bccf7d200c767c6"}, - "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, - "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, - "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "protobuf": {:hex, :protobuf, "0.9.0", "9c1633ecc098f3d7ec0a00503e070541b0e1868114fff41523934888442319e7", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "15fb7cddc5f85b8055fedaf81a9093020e4cd283647a21deb8f7de8d243abb9d"}, + "makeup_erlang": {:hex, :makeup_erlang, "0.1.4", "29563475afa9b8a2add1b7a9c8fb68d06ca7737648f28398e04461f008b69521", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f4ed47ecda66de70dd817698a703f8816daa91272e7e45812469498614ae8b29"}, + "mint": {:hex, :mint, "1.5.2", "4805e059f96028948870d23d7783613b7e6b0e2fb4e98d720383852a760067fd", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "d77d9e9ce4eb35941907f1d3df38d8f750c357865353e21d335bdcdf6d892a02"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "protobuf": {:hex, :protobuf, "0.14.1", "9ac0582170df27669ccb2ef6cb0a3d55020d58896edbba330f20d0748881530a", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "39a9d49d346e3ed597e5ae3168a43d9603870fc159419617f584cdf6071f0e25"}, + "protobuf_generate": {:hex, :protobuf_generate, "0.1.3", "57841bc60e2135e190748119d83f78669ee7820c0ad6555ada3cd3cd7df93143", [:mix], [{:protobuf, "~> 0.12", [hex: :protobuf, repo: "hexpm", optional: false]}], "hexpm", "dae4139b00ba77a279251a0ceb5593b1bae745e333b4ce1ab7e81e8e4906016b"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, } diff --git a/priv/templates/service.ex.eex b/priv/templates/service.ex.eex new file mode 100644 index 000000000..bbea50b62 --- /dev/null +++ b/priv/templates/service.ex.eex @@ -0,0 +1,24 @@ +defmodule <%= @module %>.Service do + <%= unless @module_doc? do %> + @moduledoc false + <% end %> + use GRPC.Service, name: <%= inspect(@service_name) %>, protoc_gen_elixir_version: "<%= @version %>" + + <%= if @descriptor_fun_body do %> + def descriptor do + # credo:disable-for-next-line + <%= @descriptor_fun_body %> + end + <% end %> + + <%= for {method_name, input, output, options} <- @methods do %> + rpc :<%= method_name %>, <%= input %>, <%= output %>, <%= options %> + <% end %> +end + +defmodule <%= @module %>.Stub do + <%= unless @module_doc? do %> + @moduledoc false + <% end %> + use GRPC.Stub, service: <%= @module %>.Service +end diff --git a/src/grpc_stream_h.erl b/src/grpc_stream_h.erl index b231fca3f..42e2c0525 100644 --- a/src/grpc_stream_h.erl +++ b/src/grpc_stream_h.erl @@ -4,10 +4,6 @@ -module(grpc_stream_h). -behavior(cowboy_stream). --ifdef(OTP_RELEASE). --compile({nowarn_deprecated_function, [{erlang, get_stacktrace, 0}]}). --endif. - -export([init/3]). -export([data/4]). -export([info/3]). @@ -37,7 +33,7 @@ -> {[{spawn, pid(), timeout()}], #state{}}. init(StreamID, Req=#{ref := Ref}, Opts) -> Env = maps:get(env, Opts, #{}), - Middlewares = maps:get(middlewares, Opts, [cowboy_router, cowboy_handler]), + Middlewares = maps:get(middlewares, Opts, ['Elixir.GRPC.Server.Adapters.Cowboy.Router', cowboy_handler]), Shutdown = maps:get(shutdown_timeout, Opts, 5000), Pid = proc_lib:spawn_link(?MODULE, request_process, [Req, Env, Middlewares]), Expect = expect(Req), @@ -325,21 +321,13 @@ send_request_body(Pid, Ref, fin, BodyLen, Data) -> %% @todo Better spec. -spec request_process(cowboy_req:req(), cowboy_middleware:env(), [module()]) -> ok. request_process(Req, Env, Middlewares) -> - OTP = erlang:system_info(otp_release), try execute(Req, Env, Middlewares) catch - exit:Reason -> - Stacktrace = erlang:get_stacktrace(), - erlang:raise(exit, {Reason, Stacktrace}, Stacktrace); - %% OTP 19 does not propagate any exception stacktraces, - %% we therefore add it for every class of exception. - _:Reason when OTP =:= "19" -> - Stacktrace = erlang:get_stacktrace(), + exit:Reason:Stacktrace -> erlang:raise(exit, {Reason, Stacktrace}, Stacktrace); - %% @todo I don't think this clause is necessary. - Class:Reason -> - erlang:raise(Class, Reason, erlang:get_stacktrace()) + Class:Reason:Stacktrace -> + erlang:raise(Class, {Reason, Stacktrace}, Stacktrace) end. execute(_, _, []) -> diff --git a/test/grpc/channel_test.exs b/test/grpc/channel_test.exs index 098ea010f..9e7d2e5f9 100644 --- a/test/grpc/channel_test.exs +++ b/test/grpc/channel_test.exs @@ -3,20 +3,69 @@ defmodule GRPC.ChannelTest do alias GRPC.Test.ClientAdapter alias GRPC.Channel - test "connect/2 works for insecure" do - {:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter) - assert %Channel{host: "10.1.0.0", port: 50051, scheme: "http", cred: nil} = channel - end + for {kind, addr} <- [{"ip", "10.0.0.1"}, {"hostname", "example.com"}] do + describe "connect/2 with http and #{kind}" do + test "works" do + {:ok, channel} = + GRPC.Stub.connect("http://#{unquote(addr)}:50051", adapter: ClientAdapter) + + assert %Channel{host: unquote(addr), port: 50051, scheme: "http", cred: nil} = channel + end + + test "errors if credential is provided" do + cred = %GRPC.Credential{ssl: []} + + assert_raise ArgumentError, "invalid option for insecure (http) address: :cred", fn -> + GRPC.Stub.connect("http://#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred) + end + end + end + + describe "connect/2 with https and #{kind}" do + test "sets default credential" do + {:ok, channel} = + GRPC.Stub.connect("https://#{unquote(addr)}:50051", adapter: ClientAdapter) + + assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: cred} = channel + + assert Keyword.has_key?(cred.ssl, :verify) + assert Keyword.has_key?(cred.ssl, :depth) + assert Keyword.has_key?(cred.ssl, :cacert_file) + end + + test "allows overriding default credentials" do + cred = %GRPC.Credential{ssl: []} - test "connect/2 works for ssl" do - cred = %{ssl: []} - {:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter, cred: cred) - assert %Channel{host: "10.1.0.0", port: 50051, scheme: "https", cred: ^cred} = channel + {:ok, channel} = + GRPC.Stub.connect("https://#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred) + + assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: ^cred} = channel + end + end + + describe "connect/2 with no scheme, #{kind} and" do + test "no cred uses http" do + {:ok, channel} = GRPC.Stub.connect("#{unquote(addr)}:50051", adapter: ClientAdapter) + assert %Channel{host: unquote(addr), port: 50051, scheme: "http", cred: nil} = channel + end + + test "cred uses https" do + cred = %{ssl: []} + + {:ok, channel} = + GRPC.Stub.connect("#{unquote(addr)}:50051", adapter: ClientAdapter, cred: cred) + + assert %Channel{host: unquote(addr), port: 50051, scheme: "https", cred: ^cred} = channel + end + end end test "connect/2 allows setting default headers" do headers = [{"authorization", "Bearer TOKEN"}] - {:ok, channel} = GRPC.Stub.connect("10.1.0.0:50051", adapter: ClientAdapter, headers: headers) - assert %Channel{host: "10.1.0.0", port: 50051, headers: ^headers} = channel + + {:ok, channel} = + GRPC.Stub.connect("http://10.0.0.1:50051", adapter: ClientAdapter, headers: headers) + + assert %Channel{host: "10.0.0.1", port: 50051, headers: ^headers} = channel end end diff --git a/test/grpc/client/adapters/gun_test.exs b/test/grpc/client/adapters/gun_test.exs new file mode 100644 index 000000000..851582358 --- /dev/null +++ b/test/grpc/client/adapters/gun_test.exs @@ -0,0 +1,87 @@ +defmodule GRPC.Client.Adapters.GunTest do + use GRPC.DataCase, async: true + + alias GRPC.Client.Adapters.Gun + + describe "connect/2" do + setup do + server_credential = build(:credential) + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0, cred: server_credential) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{ + port: port, + credential: server_credential + } + end + + test "connects insecurely (default options)", %{port: port, credential: credential} do + channel = build(:channel, port: port, host: "localhost", cred: credential) + + assert {:ok, result} = Gun.connect(channel, []) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects insecurely (custom options)", %{port: port, credential: credential} do + channel = build(:channel, port: port, host: "localhost", cred: credential) + + # Ensure that it works + assert {:ok, result} = Gun.connect(channel, transport_opts: [ip: :loopback]) + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options breaks things + assert {:error, {:down, :badarg}} == + Gun.connect(channel, transport_opts: [ip: "256.0.0.0"]) + end + + test "connects securely (default options)", %{port: port, credential: credential} do + channel = + build(:channel, + port: port, + scheme: "https", + host: "localhost", + cred: credential + ) + + assert {:ok, result} = Gun.connect(channel, tls_opts: channel.cred.ssl) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects securely (custom options)", %{port: port, credential: credential} do + channel = + build(:channel, + port: port, + scheme: "https", + host: "localhost", + cred: credential + ) + + # Ensure that it works + assert {:ok, result} = + Gun.connect(channel, + transport_opts: [ + verify: :verify_none, + certfile: credential.ssl[:certfile], + ip: :loopback + ] + ) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options breaks things + assert {:error, :timeout} == + Gun.connect(channel, + transport_opts: [ + certfile: credential.ssl[:certfile] <> "invalidsuffix", + verify: :verify_peer, + ip: :loopback + ] + ) + end + end +end diff --git a/test/grpc/client/adapters/mint/connection_process_test.exs b/test/grpc/client/adapters/mint/connection_process_test.exs new file mode 100644 index 000000000..0a2105a07 --- /dev/null +++ b/test/grpc/client/adapters/mint/connection_process_test.exs @@ -0,0 +1,458 @@ +defmodule GRPC.Client.Adapters.Mint.ConnectionProcessTest do + use GRPC.DataCase + alias GRPC.Client.Adapters.Mint.ConnectionProcess + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + + import ExUnit.CaptureLog + + setup do + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{port: port} + end + + describe "start_link/4" do + test "non-successful connection stops the connection process without exiting it's caller" do + Process.flag(:trap_exit, true) + + logs = + capture_log(fn -> + assert {:error, %Mint.TransportError{reason: :econnrefused}} == + ConnectionProcess.start_link(:http, "localhost", 12345) + end) + + assert logs =~ "unable to establish a connection" + end + + test "connects insecurely (default options)", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + assert Process.alive?(pid) + end + end + + describe "disconnect/1" do + test "stop the process when disconnecting", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + assert :ok == ConnectionProcess.disconnect(pid) + refute Process.alive?(pid) + end + + test "close the connection when disconnect", %{port: port} do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port) + state = :sys.get_state(pid) + + assert {:stop, :normal, :ok, new_state} = + ConnectionProcess.handle_call(:disconnect, nil, state) + + refute Mint.HTTP.open?(new_state.conn) + end + end + + describe "handle_call/2 - request - :stream" do + setup :valid_connection + + test "start stream request and put empty state for ref", %{ + request: {method, path, headers}, + state: state + } do + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:ok, %{request_ref: request_ref}}, new_state} = response + assert is_reference(request_ref) + + assert %{ + stream_response_pid: self(), + done: false, + response: %{} + } == new_state.requests[request_ref] + + assert state.conn != new_state.conn + end + + test "returns error when connection is closed", %{ + request: {method, path, headers}, + state: state + } do + {:ok, conn} = Mint.HTTP.close(state.conn) + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, %{state | conn: conn}) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn != new_state.conn + assert "the connection is closed" == error + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + request = {:request, method, path, headers, :stream, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn == new_state.conn + assert %Mint.TransportError{__exception__: true, reason: :closed} == error + end + end + + describe "handle_call/2 - request - payload" do + setup :valid_connection + + test "start stream request, enqueue payload to be process and continue", %{ + request: {method, path, headers}, + state: state + } do + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:ok, %{request_ref: request_ref}}, new_state, + {:continue, :process_request_stream_queue}} = response + + assert is_reference(request_ref) + + assert %{ + stream_response_pid: self(), + done: false, + response: %{} + } == new_state.requests[request_ref] + + assert {[{request_ref, body, nil}], []} == new_state.request_stream_queue + assert state.conn != new_state.conn + end + + test "returns error response when connection is closed", %{ + request: {method, path, headers}, + state: state + } do + {:ok, conn} = Mint.HTTP.close(state.conn) + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + response = ConnectionProcess.handle_call(request, nil, %{state | conn: conn}) + + assert {:reply, {:error, error}, new_state} = response + assert state.conn != new_state.conn + assert "the connection is closed" == error + end + + test "returns error response when mint returns an error when starting stream request", %{ + request: {method, path, headers}, + state: state + } do + body = <<1, 2, 3>> + request = {:request, method, path, headers, body, [stream_response_pid: self()]} + + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + response = ConnectionProcess.handle_call(request, nil, state) + + assert {:reply, {:error, error}, _new_state} = response + assert %Mint.TransportError{reason: :closed} == error + end + end + + describe "handle_call/2 - stream_body" do + setup :valid_connection + setup :valid_stream_request + + test "reply with :ok when stream :eof is successful", %{ + request_ref: request_ref, + state: state + } do + response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, state) + assert {:reply, :ok, new_state} = response + assert new_state.conn != state.conn + end + + test "reply with error when stream :eof is errors", %{request_ref: request_ref, state: state} do + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + response = ConnectionProcess.handle_call({:stream_body, request_ref, :eof}, nil, state) + + assert {:reply, {:error, error}, _new_state} = response + assert %Mint.TransportError{__exception__: true, reason: :closed} == error + end + + test "continue to process payload stream", %{request_ref: request_ref, state: state} do + response = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + + assert {:noreply, new_state, {:continue, :process_request_stream_queue}} = response + assert {[{request_ref, <<1, 2, 3>>, self()}], []} == new_state.request_stream_queue + assert new_state.conn == state.conn + end + end + + describe "handle_call/2 - cancel_request" do + setup :valid_connection + setup :valid_stream_request + setup :valid_stream_response + + test "reply with :ok when canceling the request is successful, also set stream response pid to done and remove request ref from state", + %{ + request_ref: request_ref, + stream_response_pid: response_pid, + state: state + } do + response = ConnectionProcess.handle_call({:cancel_request, request_ref}, nil, state) + assert {:reply, :ok, new_state} = response + assert %{} == new_state.requests + response_state = :sys.get_state(response_pid) + assert :queue.is_empty(response_state.responses) + assert true == response_state.done + end + end + + describe "handle_continue/2 - :process_stream_queue" do + setup :valid_connection + setup :valid_stream_request + setup :valid_stream_response + + test "do nothing when there is no window_size in the connection", %{ + request_ref: request_ref, + state: state + } do + # hacky to simulate a window size of zero since this is usually updated with the requests interaction + state = %{state | conn: %{state.conn | window_size: 0}} + # enqueue the payload onto the request queue + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + + assert {:noreply, new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert new_state == state + end + + test "(body_size > window_size) chunk payload stream what is possible and enqueue the rest at the begining of the queue to give priority to the current request", + %{request_ref: request_ref, state: state} do + # hacky to simulate a window size of 2 bytes since this is usually updated with the requests interaction + state = %{state | conn: %{state.conn | window_size: 2}} + # enqueue the payload onto the request queue. Add to items to the queue, + # this way we can check is the rest is of the payload goes to the first position to the queue + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, self(), state) + + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<4, 5, 6>>}, self(), state) + + assert {:noreply, new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + # mint update window_size for us. + # This is how we check if the body was streamed + assert new_state.conn.window_size == 0 + assert {{:value, head_of_queue}, queue} = :queue.out(new_state.request_stream_queue) + assert {{:value, rest}, {[], []}} = :queue.out(queue) + + # <<1, 2, 3>> got enqueue first, we streamed 2 bytes, now we have only one left <<3>> + assert {request_ref, <<3>>, self()} == head_of_queue + + # Next to be processed is <<4, 5, 6>> + assert {request_ref, <<4, 5, 6>>, self()} == rest + end + + test "(window_size >= body_size) stream bod, send end_stream message and reply caller process (when process ref is present)", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, :ok}, 500 + end + + test "(window_size >= body_size) stream body, send end_stream message and don't reply caller process (when precess ref is nil)", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1, 2, 3>>}, nil, state) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + refute_receive {:tag, :ok}, 500 + end + + test "(window_size >= body_size) stream body, send end_stream message and check request_queue when queue is not empty", + %{request_ref: request_ref, state: state} do + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<4, 5, 6>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state, {:continue, :process_request_stream_queue}} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, :ok}, 500 + end + + test "send error to the caller process when server return an error and there is a process ref", + %{request_ref: request_ref, state: state} do + # Simulates the server closing the connection before we update the state + {:ok, _conn} = Mint.HTTP.close(state.conn) + + {_, state, _} = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + assert_receive {:tag, {:error, %Mint.TransportError{reason: :closed, __exception__: true}}}, + 500 + end + + test "send error message to stream response process when caller process ref is empty", + %{request_ref: request_ref, state: state, stream_response_pid: response_pid} do + {_, state, _} = + ConnectionProcess.handle_call({:stream_body, request_ref, <<1>>}, nil, state) + + # Close connection to simulate an error (like the server closing the connection before we update state) + {:ok, _conn} = Mint.HTTP.close(state.conn) + + assert {:noreply, _new_state} = + ConnectionProcess.handle_continue(:process_request_stream_queue, state) + + response_state = :sys.get_state(response_pid) + + assert :queue.to_list(response_state.responses) == [ + error: %Mint.TransportError{reason: :closed, __exception__: true} + ] + end + end + + describe "handle_info - connection_closed - no requests" do + setup :valid_connection + + test "send a message to parent process to inform the connection is down", %{ + state: state + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + assert pid == self() + end + end + + describe "handle_info - connection_closed - with request" do + setup :valid_connection + setup :valid_stream_request + setup :valid_stream_response + + test "send a message to parent process to inform the connection is down and end stream response process", + %{ + state: state, + stream_response_pid: response_pid + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + response_state = :sys.get_state(response_pid) + assert :queue.to_list(response_state.responses) == [error: "the connection is closed"] + assert true == response_state.done + assert pid == self() + end + + test "send a message to parent process to inform the connection is down and reply pending process", + %{ + state: state, + request_ref: request_ref, + stream_response_pid: response_pid + } do + socket = state.conn.socket + # this is a mocked message to inform the connection is closed + tcp_message = {:tcp_closed, socket} + + response = + ConnectionProcess.handle_call( + {:stream_body, request_ref, <<1, 2, 3>>}, + {self(), :tag}, + state + ) + + {:noreply, state, {:continue, :process_request_stream_queue}} = response + + assert {:noreply, new_state} = ConnectionProcess.handle_info(tcp_message, state) + assert new_state.conn.state == :closed + assert_receive {:elixir_grpc, :connection_down, pid}, 500 + response_state = :sys.get_state(response_pid) + assert :queue.to_list(response_state.responses) == [error: "the connection is closed"] + assert true == response_state.done + assert pid == self() + end + end + + defp valid_connection(%{port: port}) do + {:ok, pid} = ConnectionProcess.start_link(:http, "localhost", port, protocols: [:http2]) + state = :sys.get_state(pid) + version = Application.spec(:grpc) |> Keyword.get(:vsn) + + headers = [ + {"content-type", "application/grpc"}, + {"user-agent", "grpc-elixir/#{version}"}, + {"te", "trailers"} + ] + + %{ + process_pid: pid, + state: state, + request: {"POST", "/routeguide.RouteGuide/RecordRoute", headers} + } + end + + defp valid_stream_request(%{request: {method, path, headers}, process_pid: pid}) do + {:ok, %{request_ref: request_ref}} = + ConnectionProcess.request(pid, method, path, headers, :stream, stream_response_pid: self()) + + state = :sys.get_state(pid) + %{request_ref: request_ref, state: state} + end + + defp valid_stream_response(%{request_ref: request_ref, state: state} = ctx) do + stream = build(:client_stream) + {:ok, pid} = StreamResponseProcess.start_link(stream, true) + state = update_stream_response_process_to_test_pid(state, request_ref, pid) + Map.merge(ctx, %{state: state, stream_response_pid: pid}) + end + + def update_stream_response_process_to_test_pid(state, request_ref, test_pid) do + request_ref_state = state.requests[request_ref] + + %{state | requests: %{request_ref => %{request_ref_state | stream_response_pid: test_pid}}} + end +end diff --git a/test/grpc/client/adapters/mint/stream_response_process_test.exs b/test/grpc/client/adapters/mint/stream_response_process_test.exs new file mode 100644 index 000000000..e67601952 --- /dev/null +++ b/test/grpc/client/adapters/mint/stream_response_process_test.exs @@ -0,0 +1,364 @@ +defmodule GRPC.Client.Adapters.Mint.StreamResponseProcessTest do + use GRPC.DataCase + use ExUnit.Parameterized + + alias GRPC.Client.Adapters.Mint.StreamResponseProcess + + setup do + state = %{ + buffer: "", + done: false, + from: nil, + grpc_stream: build(:client_stream), + responses: :queue.new(), + compressor: nil, + send_headers_or_trailers: false + } + + %{state: state} + end + + describe "handle_call/3 - data" do + setup do + part_1 = <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108>> + part_2 = <<108, 111, 32, 76, 117, 105, 115>> + full_message = part_1 <> part_2 + %{data: {part_1, part_2, full_message}} + end + + test "append message to buffer when message is incomplete", %{ + state: state, + data: {part1, _, _} + } do + response = + StreamResponseProcess.handle_call({:consume_response, {:data, part1}}, self(), state) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == part1 + end + + test "decode full message when incoming date is complete", %{ + state: state, + data: {_, _, full_message} + } do + expected_response_message = {:ok, build(:hello_reply_rpc)} + + response = + StreamResponseProcess.handle_call( + {:consume_response, {:data, full_message}}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == <<>> + assert :queue.to_list(new_state.responses) == [expected_response_message] + end + + test "append incoming message to existing buffer", %{state: state, data: {part1, part2, _}} do + state = %{state | buffer: part1} + expected_response_message = {:ok, build(:hello_reply_rpc)} + + response = + StreamResponseProcess.handle_call({:consume_response, {:data, part2}}, self(), state) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == <<>> + assert :queue.to_list(new_state.responses) == [expected_response_message] + end + + test "decode message and put rest on buffer", %{state: state, data: {_, _, full}} do + extra_data = <<0, 1, 2>> + data = full <> extra_data + expected_response_message = {:ok, build(:hello_reply_rpc)} + + response = + StreamResponseProcess.handle_call({:consume_response, {:data, data}}, self(), state) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert new_state.buffer == extra_data + assert :queue.to_list(new_state.responses) == [expected_response_message] + end + end + + describe "handle_call/3 - headers/trailers" do + test_with_params( + "put error in responses when incoming headers has error status", + %{state: state}, + fn %{type: type, is_header_enabled: header_enabled?} -> + state = %{state | send_headers_or_trailers: header_enabled?} + + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", "Internal Server Error"}, + {"grpc-status", "2"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_call( + {:consume_response, {type, headers}}, + self(), + state + ) + + expected_error = {:error, %GRPC.RPCError{message: "Internal Server Error", status: 2}} + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert :queue.to_list(new_state.responses) == [expected_error] + end, + do: [ + {%{type: :headers, is_header_enabled: false}}, + {%{type: :headers, is_header_enabled: true}}, + {%{type: :trailers, is_header_enabled: true}}, + {%{type: :trailers, is_header_enabled: false}} + ] + ) + + test_with_params( + "append headers to response when headers are enabled", + %{state: state}, + fn type -> + state = %{state | send_headers_or_trailers: true} + + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_call( + {:consume_response, {type, headers}}, + self(), + state + ) + + expected_response = {type, Map.new(headers)} + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert :queue.to_list(new_state.responses) == [expected_response] + end, + do: [{:headers}, {:trailers}] + ) + + test_with_params( + "skip produce headers when flag is disabled and there are no errors", + %{state: state}, + fn type -> + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + response = + StreamResponseProcess.handle_call( + {:consume_response, {type, headers}}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert :queue.is_empty(new_state.responses) + end, + do: [{:headers}, {:trailers}] + ) + + test "add compressor to state when incoming headers match available compressor", %{ + state: state + } do + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"}, + {"grpc-encoding", "gzip"} + ] + + response = + StreamResponseProcess.handle_call( + {:consume_response, {:headers, headers}}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert GRPC.Compressor.Gzip == new_state.compressor + end + + test "don't update compressor when unsupported compressor is returned by the server", %{ + state: state + } do + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"}, + {"grpc-encoding", "suzana"} + ] + + response = + StreamResponseProcess.handle_call( + {:consume_response, {:headers, headers}}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert nil == new_state.compressor + end + end + + describe "handle_call/3 - errors" do + test "add error tuple to responses", %{state: state} do + error = {:error, "howdy"} + + response = + StreamResponseProcess.handle_call( + {:consume_response, error}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert :queue.to_list(new_state.responses) == [error] + end + end + + describe "handle_call/3 - done" do + test "set state to done", %{state: state} do + response = + StreamResponseProcess.handle_call( + {:consume_response, :done}, + self(), + state + ) + + assert {:reply, :ok, new_state, {:continue, :produce_response}} = response + assert true == new_state.done + end + end + + describe "handle_continue/2 - produce_response" do + test "noreply when process ref is empty", %{state: state} do + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + assert new_state == state + end + + test "send nil message to caller process (ends Elixir.Stream) when all responses are sent and stream has ended (done: true)", + %{state: state} do + state = %{state | from: {self(), :tag}, done: true} + + {:stop, :normal, _new_state} = + StreamResponseProcess.handle_continue(:produce_response, state) + + assert_receive {:tag, nil} + end + + test "continue when there are no response to be sent and stream is not done yet", %{ + state: state + } do + state = %{state | from: {self(), :tag}, done: false} + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + assert state == new_state + end + + test "send response to caller when there are responses in the queue", %{state: state} do + state = %{state | from: {self(), :tag}, done: false, responses: :queue.from_list([1, 2])} + {:noreply, new_state} = StreamResponseProcess.handle_continue(:produce_response, state) + %{from: from, responses: responses} = new_state + assert is_nil(from) + assert :queue.to_list(responses) == [2] + assert_receive {:tag, 1} + end + end + + describe "build_stream/1" do + setup do + {:ok, pid} = StreamResponseProcess.start_link(build(:client_stream), true) + + %{pid: pid} + end + + test "ends stream when done message is passed", %{pid: pid} do + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.done(pid) + assert Enum.to_list(stream) == [] + end + + test "emits error tuple on stream when error is given to consume", %{pid: pid} do + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, :error, "an error") + StreamResponseProcess.done(pid) + assert [error] = Enum.to_list(stream) + assert {:error, "an error"} == error + end + + test "emits an ok tuple with data", %{pid: pid} do + data_to_consume = <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108, 108, 111, 32, 76, 117, 105, 115>> + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, :data, data_to_consume) + StreamResponseProcess.done(pid) + assert [data] = Enum.to_list(stream) + assert {:ok, build(:hello_reply_rpc)} == data + end + + test "preserves response messages order", %{pid: pid} do + hello_luis = + <<0, 0, 0, 0, 12, 10, 10, 72, 101, 108, 108, 111, 32, 76, 117, 105, 115>> + + bye_luis = + <<0, 0, 0, 0, 10, 10, 8, 66, 121, 101, 32, 76, 117, 105, 115>> + + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, :data, hello_luis) + StreamResponseProcess.consume(pid, :data, bye_luis) + StreamResponseProcess.done(pid) + + expected_elements = + [ + ok: build(:hello_reply_rpc), + ok: build(:bye_reply_rpc) + ] + + assert Enum.to_list(stream) == expected_elements + end + + test_with_params( + "emits headers to stream", + %{pid: pid}, + fn type -> + headers = [ + {"content-length", "0"}, + {"content-type", "application/grpc+proto"}, + {"grpc-message", ""}, + {"grpc-status", "0"}, + {"server", "Cowboy"} + ] + + stream = StreamResponseProcess.build_stream(pid) + StreamResponseProcess.consume(pid, type, headers) + StreamResponseProcess.done(pid) + assert [{response_type, response_headers}] = Enum.to_list(stream) + assert type == response_type + + assert %{ + "content-length" => "0", + "content-type" => "application/grpc+proto", + "grpc-message" => "", + "grpc-status" => "0", + "server" => "Cowboy" + } == response_headers + end, + do: [{:headers}, {:trailers}] + ) + end +end diff --git a/test/grpc/client/adapters/mint_test.exs b/test/grpc/client/adapters/mint_test.exs new file mode 100644 index 000000000..261235fd3 --- /dev/null +++ b/test/grpc/client/adapters/mint_test.exs @@ -0,0 +1,82 @@ +defmodule GRPC.Client.Adapters.MintTest do + use GRPC.DataCase + + alias GRPC.Client.Adapters.Mint + + describe "connect/2" do + setup do + {:ok, _, port} = GRPC.Server.start(FeatureServer, 0) + + on_exit(fn -> + :ok = GRPC.Server.stop(FeatureServer) + end) + + %{port: port} + end + + test "connects insecurely (default options)", %{port: port} do + channel = build(:channel, adapter: Mint, port: port, host: "localhost") + + assert {:ok, result} = Mint.connect(channel, []) + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + end + + test "connects insecurely (custom options)", %{port: port} do + channel = build(:channel, adapter: Mint, port: port, host: "localhost") + + assert {:ok, result} = Mint.connect(channel, transport_opts: [ip: :loopback]) + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options breaks things + assert {:error, message} = Mint.connect(channel, transport_opts: [ip: "256.0.0.0"]) + + assert message == "Error while opening connection: {:error, :badarg}" + end + + test "accepts config_options for application specific configuration", %{port: port} do + channel = build(:channel, adapter: Mint, port: port, host: "localhost") + + assert {:ok, result} = + Mint.connect(channel, config_options: [transport_opts: [ip: :loopback]]) + + assert %{channel | adapter_payload: %{conn_pid: result.adapter_payload.conn_pid}} == result + + # Ensure that changing one of the options via config_options also breaks things + assert {:error, message} = + Mint.connect(channel, config_options: [transport_opts: [ip: "256.0.0.0"]]) + + assert message == "Error while opening connection: {:error, :badarg}" + end + + test "defaults client settings when none is passed", %{port: port} do + channel = build(:channel, adapter: Mint, port: port, host: "localhost") + + assert {:ok, result} = Mint.connect(channel, []) + # wait for settings to be pushed + Process.sleep(50) + state = :sys.get_state(result.adapter_payload.conn_pid) + + assert %{initial_window_size: 8_000_000, max_frame_size: 8_000_000} = + Map.get(state.conn, :client_settings) + end + + test "allow client settings to be passed", %{port: port} do + channel = build(:channel, adapter: Mint, port: port, host: "localhost") + + assert {:ok, result} = + Mint.connect(channel, + client_settings: [ + initial_window_size: 50_000, + max_frame_size: 50_000 + ] + ) + + # wait for settings to be pushed + Process.sleep(50) + state = :sys.get_state(result.adapter_payload.conn_pid) + + assert %{initial_window_size: 50_000, max_frame_size: 50_000} = + Map.get(state.conn, :client_settings) + end + end +end diff --git a/test/grpc/client/interceptors/logger_test.exs b/test/grpc/client/interceptors/logger_test.exs new file mode 100644 index 000000000..35539a3a2 --- /dev/null +++ b/test/grpc/client/interceptors/logger_test.exs @@ -0,0 +1,135 @@ +defmodule GRPC.Client.Interceptors.LoggerTest do + use ExUnit.Case, async: false + + import ExUnit.CaptureLog + + alias GRPC.Client.Interceptors.Logger, as: LoggerInterceptor + alias GRPC.Client.Stream + + defmodule FakeRequest do + defstruct [] + end + + @service_name "service_name" + @rpc {1, 2, 3} + + setup do + log_level = Logger.level() + on_exit(fn -> Logger.configure(level: log_level) end) + end + + test "logs info-level by default" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn _stream, _request -> {:ok, :ok} end + opts = LoggerInterceptor.init([]) + + logs = + capture_log(fn -> + LoggerInterceptor.call(stream, request, next, opts) + end) + + assert logs =~ + ~r/\[info\]\s+Call #{@service_name}\.#{to_string(elem(@rpc, 0))} -> :ok \(\d+\.\d+ ms|µs\)/ + end + + test "allows customizing log level" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn _stream, _request -> {:ok, :ok} end + opts = LoggerInterceptor.init(level: :warning) + + logs = + capture_log(fn -> + LoggerInterceptor.call(stream, request, next, opts) + end) + + assert logs =~ + ~r/\[warn(?:ing)?\]\s+Call #{@service_name}\.#{to_string(elem(@rpc, 0))} -> :ok \(\d+\.\d+ ms|µs\)/ + end + + test "logs stream requests" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :client_stream, rpc: @rpc, service_name: @service_name} + next = fn _stream, _request -> {:ok, :stream} end + opts = LoggerInterceptor.init([]) + + logs = + capture_log(fn -> + LoggerInterceptor.call(stream, request, next, opts) + end) + + assert logs =~ + ~r/\[info\]\s+Call #{to_string(elem(@rpc, 0))} of #{@service_name}/ + end + + @tag capture_log: true + test "calls next when above :logger level" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn stream, req -> send(self(), {:next_called, stream, req}) end + opts = LoggerInterceptor.init(level: :info) + + LoggerInterceptor.call(stream, request, next, opts) + + assert_receive {:next_called, ^stream, ^request} + end + + test "calls next when below :logger level" do + Logger.configure(level: :warning) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn stream, req -> send(self(), {:next_called, stream, req}) end + opts = LoggerInterceptor.init(level: :info) + + LoggerInterceptor.call(stream, request, next, opts) + + assert_receive {:next_called, ^stream, ^request} + end + + test "logs error when next raises" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn _stream, _request -> raise GRPC.RPCError, status: :invalid_argument end + opts = LoggerInterceptor.init(level: :info) + + assert_raise(GRPC.RPCError, fn -> + logs = + capture_log(fn -> + LoggerInterceptor.call(stream, request, next, opts) + end) + + assert logs =~ + ~r/\[error\]\s+Call #{@service_name}\.#{to_string(elem(@rpc, 0))} -> %GRPC.RPCError{status: 3, message: "Client specified an invalid argument"}/ + end) + end + + test "does not log when error is not a GRPC.RPCError" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{grpc_type: :unary, rpc: @rpc, service_name: @service_name} + next = fn _stream, _request -> raise "oops" end + opts = LoggerInterceptor.init(level: :info) + + assert_raise(RuntimeError, fn -> + logs = + capture_log(fn -> + LoggerInterceptor.call(stream, request, next, opts) + end) + + assert logs == "" + end) + end +end diff --git a/test/grpc/integration/client_interceptor_test.exs b/test/grpc/integration/client_interceptor_test.exs index b6897a251..a699a0dc3 100644 --- a/test/grpc/integration/client_interceptor_test.exs +++ b/test/grpc/integration/client_interceptor_test.exs @@ -7,11 +7,13 @@ defmodule GRPC.Integration.ClientInterceptorTest do def say_hello(req, stream) do headers = GRPC.Stream.get_headers(stream) label = headers["x-test-label"] - Helloworld.HelloReply.new(message: "Hello, #{req.name} #{label}") + %Helloworld.HelloReply{message: "Hello, #{req.name} #{label}"} end end defmodule AddHeadersClientInterceptor do + @behaviour GRPC.Client.Interceptor + def init(label), do: label def call(%{headers: headers} = stream, req, next, label) do @@ -26,13 +28,36 @@ defmodule GRPC.Integration.ClientInterceptorTest do end end + defmodule RaiseClientInterceptor do + @behaviour GRPC.Client.Interceptor + + def init(opts), do: opts + + def call(_stream, _req, _next, %{ + error_function: error_function, + delay: delay, + message: message + }) do + Process.sleep(delay) + error_function.(message) + end + end + defmodule HelloEndpoint do use GRPC.Endpoint - run HelloServer + run(HelloServer) end test "client sends headers" do + client_prefix = GRPC.Telemetry.client_rpc_prefix() + stop_client_name = client_prefix ++ [:stop] + service_name = Helloworld.Greeter.Service.__meta__(:name) + + attach_events([ + stop_client_name + ]) + run_endpoint(HelloEndpoint, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", @@ -42,9 +67,83 @@ defmodule GRPC.Integration.ClientInterceptorTest do ] ) - req = Helloworld.HelloRequest.new(name: "Elixir") + req = %Helloworld.HelloRequest{name: "Elixir"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, Elixir one two" + + assert_received {^stop_client_name, _measurements, metadata} + assert %{stream: stream, request: ^req} = metadata + + assert %{ + channel: ^channel, + service_name: ^service_name, + method_name: "SayHello" + } = stream end) end + + test "sends exception event upon client exception" do + message = "exception-#{inspect(self())}" + + for {function, kind, reason} <- [ + {&throw/1, :throw, message}, + {&:erlang.exit/1, :exit, message}, + {&raise/1, :error, %RuntimeError{message: message}}, + {&:erlang.error/1, :error, %ErlangError{original: message}} + ] do + client_prefix = GRPC.Telemetry.client_rpc_prefix() + stop_client_name = client_prefix ++ [:stop] + exception_client_name = client_prefix ++ [:exception] + + attach_events([ + stop_client_name, + exception_client_name + ]) + + run_endpoint(HelloEndpoint, fn port -> + delay = floor(:rand.uniform() * 500) + 500 + + {:ok, channel} = + GRPC.Stub.connect("localhost:#{port}", + interceptors: [ + {RaiseClientInterceptor, + %{error_function: function, message: message, delay: delay}} + ] + ) + + req = %Helloworld.HelloRequest{name: "Elixir"} + + try do + Helloworld.Greeter.Stub.say_hello(channel, req) + rescue + _ -> :ok + catch + _, _ -> :ok + else + _ -> flunk("did not raise") + end + + assert_received {^exception_client_name, measurements, metadata} + assert %{duration: duration} = measurements + assert duration > delay + + assert %{kind: ^kind, reason: ^reason, stacktrace: stacktrace} = metadata + + assert is_list(stacktrace) + + Enum.each(stacktrace, fn entry -> + # ensure stacktrace is a pure stacktrace + assert {mod, fun, arity, meta} = entry + assert is_atom(mod) + assert is_atom(fun) + assert is_integer(arity) + assert is_list(meta) + end) + end) + + assert_receive {:gun_down, _, _, _, _} + + refute_receive _ + end + end end diff --git a/test/grpc/integration/codec_test.exs b/test/grpc/integration/codec_test.exs index ef28b96a5..bdd63f058 100644 --- a/test/grpc/integration/codec_test.exs +++ b/test/grpc/integration/codec_test.exs @@ -20,32 +20,30 @@ defmodule GRPC.Integration.CodecTest do defmodule HelloServer do use GRPC.Server, service: Helloworld.Greeter.Service, - codecs: [GRPC.Codec.Proto, GRPC.Codec.Erlpack] + codecs: [GRPC.Codec.Proto, GRPC.Codec.Erlpack, GRPC.Codec.WebText] def say_hello(req, _stream) do - Helloworld.HelloReply.new(message: "Hello, #{req.name}") + %Helloworld.HelloReply{message: "Hello, #{req.name}"} end end - defmodule HelloErlpackStub do + defmodule HelloStub do use GRPC.Stub, service: Helloworld.Greeter.Service end - test "Says hello over erlpack" do + test "Says hello over erlpack, GRPC-web-text" do run_server(HelloServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "Mairbek" - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} - {:ok, reply} = channel |> HelloErlpackStub.say_hello(req, codec: GRPC.Codec.Erlpack) - assert reply.message == "Hello, #{name}" - - # verify that proto still works - {:ok, reply} = channel |> HelloErlpackStub.say_hello(req, codec: GRPC.Codec.Proto) - assert reply.message == "Hello, #{name}" + for codec <- [GRPC.Codec.Erlpack, GRPC.Codec.WebText, GRPC.Codec.Proto] do + {:ok, reply} = HelloStub.say_hello(channel, req, codec: codec) + assert reply.message == "Hello, #{name}" + end # codec not registered - {:error, reply} = channel |> HelloErlpackStub.say_hello(req, codec: NotRegisteredCodec) + {:error, reply} = HelloStub.say_hello(channel, req, codec: NotRegisteredCodec) assert %GRPC.RPCError{ status: GRPC.Status.unimplemented(), @@ -53,4 +51,23 @@ defmodule GRPC.Integration.CodecTest do } == reply end) end + + test "sets the correct content-type based on codec name" do + run_server(HelloServer, fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + name = "Mairbek" + req = %Helloworld.HelloRequest{name: name} + + for {expected_content_type, codec} <- [ + {"grpc-web-text", GRPC.Codec.WebText}, + {"grpc+erlpack", GRPC.Codec.Erlpack}, + {"grpc+proto", GRPC.Codec.Proto} + ] do + {:ok, _reply, headers} = + HelloStub.say_hello(channel, req, codec: codec, return_headers: true) + + assert headers[:headers]["content-type"] == "application/#{expected_content_type}" + end + end) + end end diff --git a/test/grpc/integration/compressor_test.exs b/test/grpc/integration/compressor_test.exs index 80d93e7ed..fe4054154 100644 --- a/test/grpc/integration/compressor_test.exs +++ b/test/grpc/integration/compressor_test.exs @@ -8,7 +8,7 @@ defmodule GRPC.Integration.CompressorTest do def say_hello(%{name: name = "only client compress"}, stream) do %{"grpc-encoding" => "gzip"} = GRPC.Stream.get_headers(stream) - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end def say_hello(%{name: name = "only server compress"}, stream) do @@ -17,13 +17,13 @@ defmodule GRPC.Integration.CompressorTest do end GRPC.Server.set_compressor(stream, GRPC.Compressor.Gzip) - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end def say_hello(%{name: name = "both compress"}, stream) do %{"grpc-encoding" => "gzip"} = GRPC.Stream.get_headers(stream) GRPC.Server.set_compressor(stream, GRPC.Compressor.Gzip) - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end end @@ -32,7 +32,7 @@ defmodule GRPC.Integration.CompressorTest do service: Helloworld.Greeter.Service def say_hello(%{name: name}, _stream) do - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end end @@ -45,7 +45,7 @@ defmodule GRPC.Integration.CompressorTest do {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "only client compress" - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} {:ok, reply, headers} = channel @@ -61,7 +61,7 @@ defmodule GRPC.Integration.CompressorTest do {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "only server compress" - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} # no accept-encoding header {:ok, reply, headers} = channel |> HelloStub.say_hello(req, return_headers: true) @@ -85,7 +85,7 @@ defmodule GRPC.Integration.CompressorTest do {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "both compress" - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} {:ok, reply, headers} = channel @@ -101,11 +101,11 @@ defmodule GRPC.Integration.CompressorTest do {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") name = "both compress" - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} - assert {:error, %GRPC.RPCError{message: _, status: 12}, _} = + assert {:error, %GRPC.RPCError{status: 12}} = channel - |> HelloStub.say_hello(req, compressor: GRPC.Compressor.Gzip, return_headers: true) + |> HelloStub.say_hello(req, compressor: GRPC.Compressor.Gzip) end) end end diff --git a/test/grpc/integration/connection_test.exs b/test/grpc/integration/connection_test.exs index 94a461861..53eb9b526 100644 --- a/test/grpc/integration/connection_test.exs +++ b/test/grpc/integration/connection_test.exs @@ -1,23 +1,11 @@ defmodule GRPC.Integration.ConnectionTest do use GRPC.Integration.TestCase - @cert_path Path.expand("./tls/server1.pem", :code.priv_dir(:grpc)) - @key_path Path.expand("./tls/server1.key", :code.priv_dir(:grpc)) - @ca_path Path.expand("./tls/ca.pem", :code.priv_dir(:grpc)) - - defmodule FeatureServer do - use GRPC.Server, service: Routeguide.RouteGuide.Service - - def get_feature(point, _stream) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") - end - end - test "reconnection works" do server = FeatureServer {:ok, _, port} = GRPC.Server.start(server, 0) - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", adapter_opts: %{retry_timeout: 10}) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", adapter_opts: [retry_timeout: 10]) assert {:ok, _} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) :ok = GRPC.Server.stop(server) {:ok, _, _} = reconnect_server(server, port) @@ -31,9 +19,9 @@ defmodule GRPC.Integration.ConnectionTest do File.rm(socket_path) {:ok, _, _} = GRPC.Server.start(server, 0, ip: {:local, socket_path}) - {:ok, channel} = GRPC.Stub.connect(socket_path, adapter_opts: %{retry_timeout: 10}) + {:ok, channel} = GRPC.Stub.connect(socket_path, adapter_opts: [retry_timeout: 10]) - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} assert {:ok, _} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) :ok = GRPC.Server.stop(server) end @@ -41,23 +29,14 @@ defmodule GRPC.Integration.ConnectionTest do test "authentication works" do server = FeatureServer - cred = - GRPC.Credential.new( - ssl: [ - certfile: @cert_path, - cacertfile: @ca_path, - keyfile: @key_path, - verify: :verify_peer, - fail_if_no_peer_cert: true - ] - ) + cred = GRPC.Factory.build(:credential, verify: :verify_peer) {:ok, _, port} = GRPC.Server.start(server, 0, cred: cred) try do - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) - client_cred = GRPC.Credential.new(ssl: [certfile: @cert_path, keyfile: @key_path]) - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", cred: client_cred) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} + + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", cred: cred) assert {:ok, _} = Routeguide.RouteGuide.Stub.get_feature(channel, point) catch error -> diff --git a/test/grpc/integration/endpoint_test.exs b/test/grpc/integration/endpoint_test.exs index f5055bd40..a4dd0c2d2 100644 --- a/test/grpc/integration/endpoint_test.exs +++ b/test/grpc/integration/endpoint_test.exs @@ -6,14 +6,14 @@ defmodule GRPC.Integration.EndpointTest do use GRPC.Server, service: Helloworld.Greeter.Service def say_hello(req, _stream) do - Helloworld.HelloReply.new(message: "Hello, #{req.name}") + %Helloworld.HelloReply{message: "Hello, #{req.name}"} end end defmodule HelloEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server, level: :info + intercept GRPC.Server.Interceptors.Logger run HelloServer end @@ -21,7 +21,7 @@ defmodule GRPC.Integration.EndpointTest do def init(_), do: [] def call(_, stream, _next, _) do - {:ok, stream, Helloworld.HelloReply.new(message: "Hello by interceptor")} + {:ok, stream, %Helloworld.HelloReply{message: "Hello by interceptor"}} end end @@ -29,7 +29,7 @@ defmodule GRPC.Integration.EndpointTest do use GRPC.Server, service: Routeguide.RouteGuide.Service def get_feature(point, _stream) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} end def list_features(rectangle, stream) do @@ -40,25 +40,25 @@ defmodule GRPC.Integration.EndpointTest do end def record_route(enum, _stream) do - Routeguide.RouteSummary.new(point_count: Enum.count(enum)) + %Routeguide.RouteSummary{point_count: Enum.count(enum)} end defp simple_feature(point) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} end end defmodule FeatureEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Server.Interceptors.Logger run FeatureServer end defmodule FeatureAndHelloHaltEndpoint do use GRPC.Endpoint - intercept GRPC.Logger.Server + intercept GRPC.Server.Interceptors.Logger run HelloServer, interceptors: [HelloHaltInterceptor] run FeatureServer end @@ -68,7 +68,7 @@ defmodule GRPC.Integration.EndpointTest do run_endpoint(HelloEndpoint, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "Elixir") + req = %Helloworld.HelloRequest{name: "Elixir"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, Elixir" end) @@ -80,10 +80,10 @@ defmodule GRPC.Integration.EndpointTest do run_endpoint(FeatureEndpoint, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) - rect = Routeguide.Rectangle.new(hi: point, lo: point) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} + rect = %Routeguide.Rectangle{hi: point, lo: point} {:ok, enum} = channel |> Routeguide.RouteGuide.Stub.list_features(rect) - loc = Routeguide.Feature.new(location: point, name: "409146138,-746188906") + loc = %Routeguide.Feature{location: point, name: "409146138,-746188906"} assert [{:ok, loc}, {:ok, loc}] == Enum.to_list(enum) end) end) =~ "GRPC.Integration.EndpointTest.FeatureServer.list_features" @@ -94,13 +94,13 @@ defmodule GRPC.Integration.EndpointTest do run_endpoint(FeatureEndpoint, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point0 = Routeguide.Point.new(latitude: 0, longitude: -1) - point1 = Routeguide.Point.new(latitude: 1, longitude: 1) + point0 = %Routeguide.Point{latitude: 0, longitude: -1} + point1 = %Routeguide.Point{latitude: 1, longitude: 1} stream = channel |> Routeguide.RouteGuide.Stub.record_route() GRPC.Stub.send_request(stream, point0) GRPC.Stub.send_request(stream, point1, end_stream: true) reply = GRPC.Stub.recv(stream) - assert {:ok, Routeguide.RouteSummary.new(point_count: 2)} == reply + assert {:ok, %Routeguide.RouteSummary{point_count: 2}} == reply end) end) =~ "GRPC.Integration.EndpointTest.FeatureServer.record_route" end @@ -110,13 +110,13 @@ defmodule GRPC.Integration.EndpointTest do run_endpoint(FeatureAndHelloHaltEndpoint, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} {:ok, feature} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) assert feature == - Routeguide.Feature.new(location: point, name: "409146138,-746188906") + %Routeguide.Feature{location: point, name: "409146138,-746188906"} - req = Helloworld.HelloRequest.new(name: "Elixir") + req = %Helloworld.HelloRequest{name: "Elixir"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello by interceptor" end) diff --git a/test/grpc/integration/erlpack_notypes.ex b/test/grpc/integration/erlpack_notypes.ex index cf230f2fa..20376523c 100644 --- a/test/grpc/integration/erlpack_notypes.ex +++ b/test/grpc/integration/erlpack_notypes.ex @@ -24,7 +24,7 @@ defmodule GRPC.Integration.ErplackNotypesTest do {:ok, channel} = GRPC.Stub.connect( "localhost:#{port}", - interceptors: [GRPC.Logger.Client], + interceptors: [GRPC.Client.Interceptors.Logger], codec: GRPC.Codec.Erlpack ) @@ -36,7 +36,9 @@ defmodule GRPC.Integration.ErplackNotypesTest do test "Says hello over erlpack call level" do run_server(HelloServer, fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", interceptors: [GRPC.Logger.Client]) + {:ok, channel} = + GRPC.Stub.connect("localhost:#{port}", interceptors: [GRPC.Client.Interceptors.Logger]) + name = "World" {:ok, reply} = channel |> HelloErlpackStub.reply_hello(name, codec: GRPC.Codec.Erlpack) assert reply == {:ok, "Hello, #{name}"} diff --git a/test/grpc/integration/namespace_test.exs b/test/grpc/integration/namespace_test.exs index f876178ed..d187ce80f 100644 --- a/test/grpc/integration/namespace_test.exs +++ b/test/grpc/integration/namespace_test.exs @@ -5,16 +5,16 @@ defmodule GRPC.Integration.NamespaceTest do use GRPC.Server, service: Routeguide.RouteGuide.Service def get_feature(point, _stream) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} end end test "it works when outer namespace is same with inner's" do run_server(FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} {:ok, feature} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) - assert feature == Routeguide.Feature.new(location: point, name: "409146138,-746188906") + assert feature == %Routeguide.Feature{location: point, name: "409146138,-746188906"} end) end end diff --git a/test/grpc/integration/server_test.exs b/test/grpc/integration/server_test.exs index 02078a0a3..d4d3727e7 100644 --- a/test/grpc/integration/server_test.exs +++ b/test/grpc/integration/server_test.exs @@ -5,41 +5,125 @@ defmodule GRPC.Integration.ServerTest do use GRPC.Server, service: Routeguide.RouteGuide.Service def get_feature(point, _stream) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} + end + + def route_chat(_ex_stream, stream) do + GRPC.Server.send_headers(stream, %{}) + Process.exit(self(), :shutdown) + Process.sleep(500) + end + end + + defmodule TranscodeErrorServer do + use GRPC.Server, + service: Transcode.Messaging.Service, + http_transcode: true + + def get_message(req, _stream) do + status = String.to_existing_atom(req.name) + + raise GRPC.RPCError, status: status + end + end + + defmodule TranscodeServer do + use GRPC.Server, + service: Transcode.Messaging.Service, + http_transcode: true + + def get_message(msg_request, _stream) do + %Transcode.Message{name: msg_request.name, text: "get_message"} + end + + def stream_messages(msg_request, stream) do + Enum.each(1..5, fn i -> + msg = %Transcode.Message{ + name: msg_request.name, + text: "#{i}" + } + + GRPC.Server.send_reply(stream, msg) + end) + end + + def create_message(msg, _stream) do + msg + end + + def create_message_with_nested_body(msg_request, _stream) do + %Transcode.Message{ + name: msg_request.message.name, + text: "create_message_with_nested_body" + } + end + + def get_message_with_field_path(msg_request, _) do + msg_request.message + end + + def get_message_with_response_body(msg_request, _) do + %Transcode.MessageOut{ + response: %Transcode.Message{ + name: msg_request.name, + text: "get_message_with_response_body" + } + } + end + + def get_message_with_query(msg_request, _stream) do + %Transcode.Message{name: msg_request.name, text: "get_message_with_query"} + end + + def get_message_with_subpath_query(msg_request, _stream) do + %Transcode.Message{ + name: msg_request.message.name, + text: "get_message_with_subpath_query" + } end end defmodule HelloServer do use GRPC.Server, service: Helloworld.Greeter.Service + def say_hello(%{name: "raise", duration: duration}, _stream) do + Process.sleep(duration) + raise ArgumentError, "exception raised" + end + + def say_hello(%{name: "delay", duration: duration}, _stream) do + Process.sleep(duration) + %Helloworld.HelloReply{message: "Hello"} + end + def say_hello(%{name: "large response"}, _stream) do name = String.duplicate("a", round(:math.pow(2, 14))) - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end def say_hello(%{name: "get peer"}, stream) do {ip, _port} = stream.adapter.get_peer(stream.payload) name = to_string(:inet_parse.ntoa(ip)) - Helloworld.HelloReply.new(message: "Hello, #{name}") + %Helloworld.HelloReply{message: "Hello, #{name}"} end def say_hello(%{name: "get cert"}, stream) do case stream.adapter.get_cert(stream.payload) do :undefined -> - Helloworld.HelloReply.new(message: "Hello, unauthenticated") + %Helloworld.HelloReply{message: "Hello, unauthenticated"} _ -> - Helloworld.HelloReply.new(message: "Hello, authenticated") + %Helloworld.HelloReply{message: "Hello, authenticated"} end end def say_hello(req, _stream) do - Helloworld.HelloReply.new(message: "Hello, #{req.name}") + %Helloworld.HelloReply{message: "Hello, #{req.name}"} end def check_headers(_req, stream) do token = GRPC.Stream.get_headers(stream)["authorization"] - Helloworld.HeaderReply.new(authorization: token) + %Helloworld.HeaderReply{authorization: token} end end @@ -50,15 +134,6 @@ defmodule GRPC.Integration.ServerTest do raise "unknown error(This is a test, please ignore it)" end - def say_hello(%{name: "error with details"}, _stream) do - detail = - Google.Rpc.QuotaFailure.new( - violations: [%{subject: "hello", description: "Limit one greeting per person"}] - ) - - raise GRPC.RPCError, status: GRPC.Status.resource_exhausted(), details: [detail] - end - def say_hello(_req, _stream) do raise GRPC.RPCError, status: GRPC.Status.unauthenticated(), message: "Please authenticate" end @@ -78,7 +153,7 @@ defmodule GRPC.Integration.ServerTest do end defp simple_feature(point) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} end end @@ -103,75 +178,89 @@ defmodule GRPC.Integration.ServerTest do end defp simple_feature(point) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} + end + end + + defmodule HTTP1Server do + def init(req, state) do + {:ok, :cowboy_req.reply(200, %{}, "OK", req), state} end end test "multiple servers works" do run_server([FeatureServer, HelloServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} {:ok, feature} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) - assert feature == Routeguide.Feature.new(location: point, name: "409146138,-746188906") + assert feature == %Routeguide.Feature{location: point, name: "409146138,-746188906"} - req = Helloworld.HelloRequest.new(name: "Elixir") + req = %Helloworld.HelloRequest{name: "Elixir"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, Elixir" end) end - test "returns appropriate error for unary requests" do - run_server([HelloErrorServer], fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "Elixir") - {:error, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) - - assert %GRPC.RPCError{ - status: GRPC.Status.unauthenticated(), - message: "Please authenticate" - } == reply - end) + test "HTTP/1 status handler can be started along a gRPC server" do + status_handler = {"/status", HTTP1Server, []} + + run_server( + [HelloServer], + fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + req = %Helloworld.HelloRequest{name: "Elixir"} + {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) + assert reply.message == "Hello, Elixir" + + {:ok, conn_pid} = :gun.open(~c"localhost", port) + stream_ref = :gun.get(conn_pid, "/status") + Process.sleep(100) + + assert_received {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + end, + 0, + adapter_opts: [status_handler: status_handler] + ) end - test "returns error details for unary requests" do - run_server([HelloErrorServer], fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "error with details") - {:error, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) - - assert %GRPC.RPCError{ - __exception__: true, - details: [ - %Google.Rpc.QuotaFailure{ - violations: [ - %Google.Rpc.QuotaFailure.Violation{ - description: "Limit one greeting per person", - subject: "hello" - } - ] - } - ], - message: "Some resource has been exhausted", - status: 8 - } == reply - end) + test "returns appropriate error for unary requests" do + logs = + ExUnit.CaptureLog.capture_log(fn -> + run_server([HelloErrorServer], fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + req = %Helloworld.HelloRequest{name: "Elixir"} + {:error, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) + + assert %GRPC.RPCError{ + status: GRPC.Status.unauthenticated(), + message: "Please authenticate" + } == reply + end) + end) + + assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello" end test "return errors for unknown errors" do - run_server([HelloErrorServer], fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "unknown error") + logs = + ExUnit.CaptureLog.capture_log(fn -> + run_server([HelloErrorServer], fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + req = %Helloworld.HelloRequest{name: "unknown error"} + + assert {:error, + %GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} == + channel |> Helloworld.Greeter.Stub.say_hello(req) + end) + end) - assert {:error, - %GRPC.RPCError{message: "Internal Server Error", status: GRPC.Status.unknown()}} == - channel |> Helloworld.Greeter.Stub.say_hello(req) - end) + assert logs =~ "Exception raised while handling /helloworld.Greeter/SayHello" end test "returns appropriate error for stream requests" do run_server([FeatureErrorServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - rect = Routeguide.Rectangle.new() + rect = %Routeguide.Rectangle{} error = %GRPC.RPCError{message: "Please authenticate", status: 16} assert {:error, ^error} = channel |> Routeguide.RouteGuide.Stub.list_features(rect) end) @@ -180,7 +269,7 @@ defmodule GRPC.Integration.ServerTest do test "return large response(more than MAX_FRAME_SIZE 16384)" do run_server([HelloServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "large response") + req = %Helloworld.HelloRequest{name: "large response"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) name = String.duplicate("a", round(:math.pow(2, 14))) assert "Hello, #{name}" == reply.message @@ -190,7 +279,7 @@ defmodule GRPC.Integration.ServerTest do test "return deadline error for slow server" do run_server([TimeoutServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - rect = Routeguide.Rectangle.new() + rect = %Routeguide.Rectangle{} error = %GRPC.RPCError{message: "Deadline expired", status: 4} assert {:error, ^error} = @@ -201,9 +290,9 @@ defmodule GRPC.Integration.ServerTest do test "return normally for a little slow server" do run_server([SlowServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - low = Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000) - high = Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - rect = Routeguide.Rectangle.new(lo: low, hi: high) + low = %Routeguide.Point{latitude: 400_000_000, longitude: -750_000_000} + high = %Routeguide.Point{latitude: 420_000_000, longitude: -730_000_000} + rect = %Routeguide.Rectangle{lo: low, hi: high} {:ok, stream} = channel |> Routeguide.RouteGuide.Stub.list_features(rect, timeout: 500) Enum.each(stream, fn {:ok, feature} -> @@ -221,8 +310,7 @@ defmodule GRPC.Integration.ServerTest do headers: [{"authorization", token}] ) - {:ok, reply} = - channel |> Helloworld.Greeter.Stub.check_headers(Helloworld.HeaderRequest.new()) + {:ok, reply} = channel |> Helloworld.Greeter.Stub.check_headers(%Helloworld.HeaderRequest{}) assert reply.authorization == token end) @@ -232,7 +320,7 @@ defmodule GRPC.Integration.ServerTest do run_server([HelloServer], fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "get peer") + req = %Helloworld.HelloRequest{name: "get peer"} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, 127.0.0.1" end) @@ -240,11 +328,440 @@ defmodule GRPC.Integration.ServerTest do test "get cert returns correct client certificate when not present" do run_server([HelloServer], fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + assert {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "get cert") - {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) + req = %Helloworld.HelloRequest{name: "get cert"} + assert {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, unauthenticated" end) end + + test "gracefully handles server shutdown disconnects" do + logs = + ExUnit.CaptureLog.capture_log(fn -> + run_server(FeatureServer, fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + client_stream = Routeguide.RouteGuide.Stub.route_chat(channel) + assert %GRPC.Client.Stream{} = client_stream + {:ok, ex_stream} = GRPC.Stub.recv(client_stream, timeout: :infinity) + assert [{:error, %GRPC.RPCError{status: 13}}] = Enum.into(ex_stream, []) + end) + end) + + assert logs == "" + end + + describe "http/json transcode" do + test "grpc method can be called using json when http_transcode == true" do + run_server([TranscodeServer], fn port -> + name = "direct_call" + + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + stream_ref = + :gun.post( + conn_pid, + "/transcode.Messaging/GetMessage", + [ + {"content-type", "application/json"} + ], + Jason.encode!(%{"name" => name}) + ) + + assert_receive {:gun_up, ^conn_pid, :http} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{"text" => "get_message"} = Jason.decode!(body) + end) + end + + test "should map grpc error codes to http status" do + run_server([TranscodeErrorServer], fn port -> + for {code_name, _status} <- [ + {"cancelled", 400}, + {"unknown", 500}, + {"invalid_argument", 400}, + {"deadline_exceeded", 504}, + {"not_found", 404}, + {"already_exists", 409}, + {"permission_denied", 403}, + {"resource_exhausted", 429}, + {"failed_precondition", 412}, + {"aborted", 409}, + {"out_of_range", 400}, + {"unimplemented", 501}, + {"internal", 500}, + {"unavailable", 503}, + {"data_loss", 500}, + {"unauthenticated", 401} + ] do + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + _stream_ref = + :gun.get( + conn_pid, + "/v1/messages/#{code_name}", + [ + {"accept", "application/json"} + ] + ) + + assert_receive {:gun_up, ^conn_pid, :http} + end + end) + end + + test "accept: application/json can be used with get requests" do + run_server([TranscodeServer], fn port -> + name = "direct_call" + + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + stream_ref = + :gun.get(conn_pid, "/v1/messages/#{name}", [ + {"accept", "application/json"} + ]) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{"text" => "get_message"} = Jason.decode!(body) + end) + end + + test "can transcode path params" do + run_server([TranscodeServer], fn port -> + name = "foo" + + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + stream_ref = + :gun.get(conn_pid, "/v1/messages/#{name}", [ + {"content-type", "application/json"} + ]) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{ + "name" => ^name, + "text" => _name + } = Jason.decode!(body) + end) + end + + test "can transcode query params" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + stream_ref = + :gun.get(conn_pid, "/v1/messages?name=some_name", [ + {"content-type", "application/json"} + ]) + + assert_receive {:gun_up, ^conn_pid, :http} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{ + "name" => "some_name", + "text" => "get_message_with_query" + } = Jason.decode!(body) + end) + end + + test "can map request body using HttpRule.body and response using HttpRule.response_body" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + body = %{"name" => "name"} + + stream_ref = + :gun.post( + conn_pid, + "/v1/messages/nested", + [ + {"content-type", "application/json"} + ], + Jason.encode!(body) + ) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{"name" => "name", "text" => "create_message_with_nested_body"} = + Jason.decode!(body) + end) + end + + test "can map response body using HttpRule.response_body" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + name = "response_body_mapper" + + stream_ref = + :gun.get( + conn_pid, + "/v1/messages/response_body/#{name}", + [ + {"content-type", "application/json"} + ] + ) + + assert_receive {:gun_up, ^conn_pid, :http} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{"name" => ^name, "text" => "get_message_with_response_body"} = + Jason.decode!(body) + end) + end + + test "can send streaming responses" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + stream_ref = + :gun.get( + conn_pid, + "/v1/messages/stream/stream_test", + [ + {"content-type", "application/json"} + ] + ) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + msgs = String.split(body, "\n", trim: true) + assert length(msgs) == 5 + end) + end + + test "can use field paths in requests" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + name = "fieldpath" + + stream_ref = + :gun.get( + conn_pid, + "/v1/messages/fieldpath/#{name}", + [ + {"content-type", "application/json"} + ] + ) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + assert %{"name" => ^name} = Jason.decode!(body) + end) + end + + test "service methods can have the same path but different methods in http rule option" do + run_server([TranscodeServer], fn port -> + {:ok, conn_pid} = :gun.open(~c"localhost", port) + + payload = %{"name" => "foo", "text" => "bar"} + + stream_ref = + :gun.post( + conn_pid, + "/v1/messages", + [ + {"content-type", "application/json"} + ], + Jason.encode!(payload) + ) + + assert_receive {:gun_up, ^conn_pid, :http} + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert ^payload = Jason.decode!(body) + + stream_ref = + :gun.get(conn_pid, "/v1/messages?name=another_name", [ + {"content-type", "application/json"} + ]) + + assert_receive {:gun_response, ^conn_pid, ^stream_ref, :nofin, 200, _headers} + + assert {:ok, body} = :gun.await_body(conn_pid, stream_ref) + + assert %{ + "name" => "another_name", + "text" => "get_message_with_query" + } = Jason.decode!(body) + end) + end + end + + describe "telemetry" do + test "sends server start+stop events on success" do + server_rpc_prefix = GRPC.Telemetry.server_rpc_prefix() + start_server_name = server_rpc_prefix ++ [:start] + stop_server_name = server_rpc_prefix ++ [:stop] + exception_server_name = server_rpc_prefix ++ [:exception] + + client_rpc_prefix = GRPC.Telemetry.client_rpc_prefix() + start_client_name = client_rpc_prefix ++ [:start] + stop_client_name = client_rpc_prefix ++ [:stop] + exception_client_name = client_rpc_prefix ++ [:exception] + + attach_events([ + start_server_name, + stop_server_name, + exception_server_name, + start_client_name, + stop_client_name, + exception_client_name + ]) + + run_server([HelloServer], fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + + req = %Helloworld.HelloRequest{name: "delay", duration: 1000} + + assert {:ok, _} = Helloworld.Greeter.Stub.say_hello(channel, req) + end) + + assert_received {^start_server_name, measurements, metadata} + assert %{monotonic_time: _, system_time: _} = measurements + + assert %{ + server: HelloServer, + endpoint: nil, + function_name: :say_hello, + stream: %GRPC.Server.Stream{} + } = metadata + + assert_received {^stop_server_name, measurements, metadata} + assert %{duration: duration} = measurements + assert duration > 1000 + + assert %{ + server: HelloServer, + endpoint: nil, + function_name: :say_hello, + stream: %GRPC.Server.Stream{} + } = metadata + + assert_received {:gun_down, _, _, _, _} + + assert_received {^start_client_name, measurements, metadata} + assert %{monotonic_time: _, system_time: _} = measurements + + assert %{ + stream: %GRPC.Client.Stream{ + rpc: + {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}, + %{}} + } + } = metadata + + assert_received {^stop_client_name, measurements, metadata} + assert %{duration: duration} = measurements + assert duration > 1100 + + assert %{ + stream: %GRPC.Client.Stream{ + rpc: + {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}, + %{}} + } + } = metadata + + refute_receive _ + end + + test "sends server start+exception events on success" do + server_rpc_prefix = GRPC.Telemetry.server_rpc_prefix() + start_server_name = server_rpc_prefix ++ [:start] + stop_server_name = server_rpc_prefix ++ [:stop] + exception_server_name = server_rpc_prefix ++ [:exception] + + client_rpc_prefix = GRPC.Telemetry.client_rpc_prefix() + start_client_name = client_rpc_prefix ++ [:start] + stop_client_name = client_rpc_prefix ++ [:stop] + exception_client_name = client_rpc_prefix ++ [:exception] + + attach_events([ + start_server_name, + stop_server_name, + exception_server_name, + start_client_name, + stop_client_name, + exception_client_name + ]) + + run_server([HelloServer], fn port -> + {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") + + req = %Helloworld.HelloRequest{name: "raise", duration: 1100} + + assert {:error, %GRPC.RPCError{status: 2}} = + Helloworld.Greeter.Stub.say_hello(channel, req) + end) + + assert_received {^start_server_name, measurements, metadata} + assert %{monotonic_time: _, system_time: _} = measurements + + assert %{ + server: HelloServer, + endpoint: nil, + function_name: :say_hello, + stream: %GRPC.Server.Stream{} + } = metadata + + assert_received {^exception_server_name, measurements, metadata} + assert %{duration: duration} = measurements + assert duration > 1100 + + assert %{ + server: HelloServer, + endpoint: nil, + function_name: :say_hello, + stream: %GRPC.Server.Stream{}, + kind: :error, + reason: %ArgumentError{message: "exception raised"}, + stacktrace: stacktrace + } = metadata + + assert is_list(stacktrace) + + Enum.each(stacktrace, fn entry -> + # ensure stacktrace is a pure stacktrace + assert {mod, fun, arity, meta} = entry + assert is_atom(mod) + assert is_atom(fun) + assert is_integer(arity) + assert is_list(meta) + end) + + assert_received {^start_client_name, measurements, metadata} + assert %{monotonic_time: _, system_time: _} = measurements + + assert %{ + stream: %GRPC.Client.Stream{ + rpc: + {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}, + %{}} + } + } = metadata + + assert_received {^stop_client_name, measurements, metadata} + assert %{duration: duration} = measurements + assert duration > 1100 + + assert %{ + stream: %GRPC.Client.Stream{ + rpc: + {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}, + %{}} + } + } = metadata + + assert_received {:gun_down, _, _, _, _} + + refute_receive _ + end + end end diff --git a/test/grpc/integration/service_test.exs b/test/grpc/integration/service_test.exs index 3da896f98..ba321e2b3 100644 --- a/test/grpc/integration/service_test.exs +++ b/test/grpc/integration/service_test.exs @@ -24,12 +24,12 @@ defmodule GRPC.Integration.ServiceTest do fake_num = length(points) - Routeguide.RouteSummary.new( + %Routeguide.RouteSummary{ point_count: fake_num, feature_count: fake_num, distance: fake_num, elapsed_time: fake_num - ) + } end def route_chat(req_enum, stream) do @@ -56,30 +56,30 @@ defmodule GRPC.Integration.ServiceTest do end defp simple_feature(point) do - Routeguide.Feature.new(location: point, name: "#{point.latitude},#{point.longitude}") + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} end end test "unary RPC works" do run_server(FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point = Routeguide.Point.new(latitude: 409_146_138, longitude: -746_188_906) + point = %Routeguide.Point{latitude: 409_146_138, longitude: -746_188_906} {:ok, feature} = channel |> Routeguide.RouteGuide.Stub.get_feature(point) - assert feature == Routeguide.Feature.new(location: point, name: "409146138,-746188906") + assert feature == %Routeguide.Feature{location: point, name: "409146138,-746188906"} end) end test "server streaming RPC works" do run_server(FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - low = Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000) - high = Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) - rect = Routeguide.Rectangle.new(lo: low, hi: high) + low = %Routeguide.Point{latitude: 400_000_000, longitude: -750_000_000} + high = %Routeguide.Point{latitude: 420_000_000, longitude: -730_000_000} + rect = %Routeguide.Rectangle{lo: low, hi: high} {:ok, stream} = channel |> Routeguide.RouteGuide.Stub.list_features(rect) assert Enum.to_list(stream) == [ - {:ok, Routeguide.Feature.new(location: low, name: "400000000,-750000000")}, - {:ok, Routeguide.Feature.new(location: high, name: "420000000,-730000000")} + {:ok, %Routeguide.Feature{location: low, name: "400000000,-750000000"}}, + {:ok, %Routeguide.Feature{location: high, name: "420000000,-730000000"}} ] end) end @@ -87,8 +87,8 @@ defmodule GRPC.Integration.ServiceTest do test "client streaming RPC works" do run_server(FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - point1 = Routeguide.Point.new(latitude: 400_000_000, longitude: -750_000_000) - point2 = Routeguide.Point.new(latitude: 420_000_000, longitude: -730_000_000) + point1 = %Routeguide.Point{latitude: 400_000_000, longitude: -750_000_000} + point2 = %Routeguide.Point{latitude: 420_000_000, longitude: -730_000_000} stream = channel |> Routeguide.RouteGuide.Stub.record_route() GRPC.Stub.send_request(stream, point1) GRPC.Stub.send_request(stream, point2, end_stream: true) @@ -105,8 +105,8 @@ defmodule GRPC.Integration.ServiceTest do task = Task.async(fn -> Enum.each(1..6, fn i -> - point = Routeguide.Point.new(latitude: 0, longitude: rem(i, 3) + 1) - note = Routeguide.RouteNote.new(location: point, message: "Message #{i}") + point = %Routeguide.Point{latitude: 0, longitude: rem(i, 3) + 1} + note = %Routeguide.RouteNote{location: point, message: "Message #{i}"} opts = if i == 6, do: [end_stream: true], else: [] GRPC.Stub.send_request(stream, note, opts) end) @@ -132,13 +132,13 @@ defmodule GRPC.Integration.ServiceTest do FeatureServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - stream = channel |> Routeguide.RouteGuide.Stub.async_route_chat() + stream = channel |> Routeguide.RouteGuide.Stub.route_chat() task = Task.async(fn -> Enum.each(1..5, fn i -> - point = Routeguide.Point.new(latitude: 0, longitude: rem(i, 3) + 1) - note = Routeguide.RouteNote.new(location: point, message: "Message #{i}") + point = %Routeguide.Point{latitude: 0, longitude: rem(i, 3) + 1} + note = %Routeguide.RouteNote{location: point, message: "Message #{i}"} # note that we don't send end of stream yet here GRPC.Stub.send_request(stream, note, []) end) @@ -152,11 +152,11 @@ defmodule GRPC.Integration.ServiceTest do notes = Enum.map(result_enum, fn {:ok, note} -> - assert "Reply: " <> msg = note.message + assert "Reply: " <> _msg = note.message if note.message == "Reply: Message 5" do - point = Routeguide.Point.new(latitude: 0, longitude: rem(6, 3) + 1) - note = Routeguide.RouteNote.new(location: point, message: "Message #{6}") + point = %Routeguide.Point{latitude: 0, longitude: rem(6, 3) + 1} + note = %Routeguide.RouteNote{location: point, message: "Message #{6}"} GRPC.Stub.send_request(stream, note, end_stream: true) end diff --git a/test/grpc/integration/stub_test.exs b/test/grpc/integration/stub_test.exs index 7a2564293..8489a621f 100644 --- a/test/grpc/integration/stub_test.exs +++ b/test/grpc/integration/stub_test.exs @@ -5,7 +5,7 @@ defmodule GRPC.Integration.StubTest do use GRPC.Server, service: Helloworld.Greeter.Service def say_hello(req, _stream) do - Helloworld.HelloReply.new(message: "Hello, #{req.name}") + %Helloworld.HelloReply{message: "Hello, #{req.name}"} end end @@ -58,18 +58,28 @@ defmodule GRPC.Integration.StubTest do test "body larger than 2^14 works" do run_server(HelloServer, fn port -> - {:ok, channel} = GRPC.Stub.connect("localhost:#{port}", interceptors: [GRPC.Logger.Client]) + {:ok, channel} = + GRPC.Stub.connect("localhost:#{port}", interceptors: [GRPC.Client.Interceptors.Logger]) + name = String.duplicate("a", round(:math.pow(2, 15))) - req = Helloworld.HelloRequest.new(name: name) + req = %Helloworld.HelloRequest{name: name} {:ok, reply} = channel |> Helloworld.Greeter.Stub.say_hello(req) assert reply.message == "Hello, #{name}" end) end + test "invalid channel function clause error" do + req = %Helloworld.HelloRequest{name: "GRPC"} + + assert_raise FunctionClauseError, ~r/Helloworld.Greeter.Stub.say_hello/, fn -> + Helloworld.Greeter.Stub.say_hello(nil, req) + end + end + test "returns error when timeout" do run_server(SlowServer, fn port -> {:ok, channel} = GRPC.Stub.connect("localhost:#{port}") - req = Helloworld.HelloRequest.new(name: "Elixir") + req = %Helloworld.HelloRequest{name: "Elixir"} assert {:error, %GRPC.RPCError{ diff --git a/test/grpc/message/protobuf_test.exs b/test/grpc/message/protobuf_test.exs deleted file mode 100644 index 3e1b8dae6..000000000 --- a/test/grpc/message/protobuf_test.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule GRPC.Message.ProtobufTest do - use ExUnit.Case, async: true - - defmodule Helloworld.HelloRequest do - use Protobuf - - defstruct [:name] - field(:name, 1, optional: true, type: :string) - end - - defmodule Helloworld.HelloReply do - use Protobuf - - defstruct [:message] - field(:message, 1, optional: true, type: :string) - end - - test "encode/2 works for matched arguments" do - request = Helloworld.HelloRequest.new(name: "elixir") - - assert <<10, 6, 101, 108, 105, 120, 105, 114>> = - GRPC.Message.Protobuf.encode(Helloworld.HelloRequest, request) - end - - test "decode/2 works" do - msg = <<10, 6, 101, 108, 105, 120, 105, 114>> - request = Helloworld.HelloRequest.new(name: "elixir") - assert ^request = GRPC.Message.Protobuf.decode(Helloworld.HelloRequest, msg) - end - - test "decode/2 returns wrong result for mismatched arguments" do - # encoded HelloRequest - msg = <<10, 6, 101, 108, 105, 120, 105, 114>> - request = Helloworld.HelloReply.new(message: "elixir") - assert ^request = GRPC.Message.Protobuf.decode(Helloworld.HelloReply, msg) - end -end diff --git a/test/grpc/message_test.exs b/test/grpc/message_test.exs index 765e76f35..04b29f325 100644 --- a/test/grpc/message_test.exs +++ b/test/grpc/message_test.exs @@ -15,4 +15,23 @@ defmodule GRPC.MessageTest do assert {:ok, message} == GRPC.Message.from_data(%{compressor: GRPC.Compressor.Gzip}, data) end + + test "iodata can be passed to and returned from `to_data/2`" do + message = List.duplicate("foo", 100) + + assert {:ok, data, 32} = + GRPC.Message.to_data(message, iolist: true, compressor: GRPC.Compressor.Gzip) + + assert is_list(data) + binary = IO.iodata_to_binary(data) + + assert {:ok, IO.iodata_to_binary(message)} == + GRPC.Message.from_data(%{compressor: GRPC.Compressor.Gzip}, binary) + end + + test "to_data/2 invokes codec.pack_for_channel on the gRPC body if codec implements it" do + message = "web-text" + assert {:ok, base64_payload, _} = GRPC.Message.to_data(message, %{codec: GRPC.Codec.WebText}) + assert message == GRPC.Message.from_data(Base.decode64!(base64_payload)) + end end diff --git a/test/grpc/rpc_error_test.exs b/test/grpc/rpc_error_test.exs new file mode 100644 index 000000000..f33458265 --- /dev/null +++ b/test/grpc/rpc_error_test.exs @@ -0,0 +1,69 @@ +defmodule GRPC.RPCErrorTest do + use GRPC.DataCase, async: true + + import GRPC.RPCError, only: [is_rpc_error: 2] + + alias GRPC.Status + alias GRPC.RPCError + + defmodule HandlingWithFunctionGuard do + @cancelled Status.cancelled() + @unknown Status.unknown() + + def handle(err) when is_rpc_error(err, @cancelled) do + :cancelled + end + + def handle(err) when is_rpc_error(err, @unknown) do + :unknown + end + + def handle(_err) do + :fallback + end + end + + describe "is_rpc_error/2 guard clause" do + test "returns true for GRPC.RPCError structs with same status" do + exception = GRPC.RPCError.new(:unknown) + + assert is_rpc_error(exception, Status.unknown()) + end + + test "returns false for GRPC.RPCError structs with different status" do + exception = GRPC.RPCError.new(:unknown) + + refute is_rpc_error(exception, Status.cancelled()) + end + + test "returns false for others types of input" do + exception = %{status: :unknown} + + refute is_rpc_error(exception, Status.unknown()) + end + + test "supports clause guarding in function heads" do + assert HandlingWithFunctionGuard.handle(GRPC.RPCError.new(:unknown)) == :unknown + assert HandlingWithFunctionGuard.handle(GRPC.RPCError.new(:cancelled)) == :cancelled + assert HandlingWithFunctionGuard.handle(GRPC.RPCError.new(:not_found)) == :fallback + end + end + + describe "new/1" do + test "creates the GRPC.RPCError struct with a default message" do + assert %RPCError{status: code, message: message} = RPCError.new(:unknown) + assert code == Status.unknown() + refute is_nil(message) + end + + test "creates the GRPC.RPCError struct with its own predefined message and status code" do + # small sampling of statuses. ok must be here because its an edge case + for status_name <- [:ok, :cancelled, :unknown] do + status = apply(Status, status_name, []) + + assert %RPCError{status: status, message: Status.status_message(status)} == + RPCError.new(status_name) + end + end + end +end diff --git a/test/grpc/server/adapter/cowboy/router_test.exs b/test/grpc/server/adapter/cowboy/router_test.exs new file mode 100644 index 000000000..9b3343a53 --- /dev/null +++ b/test/grpc/server/adapter/cowboy/router_test.exs @@ -0,0 +1,168 @@ +defmodule GRPC.Server.Adapters.Cowboy.RouterTest do + use ExUnit.Case, async: true + alias GRPC.Server.Adapters.Cowboy.Router + + describe "match/3" do + test "with no_host" do + assert {:error, :notfound, :host} = Router.match([], [], []) + end + + test "with no bindings" do + dispatch = make_dispatch("/transcode.Messaging/GetMessage") + + assert {:ok, Handler, [], %{}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/transcode.Messaging/GetMessage") + + assert {:error, :notfound, :path} == Router.match(dispatch, "localhost", "/unknown/path") + end + + test "with simple bindings" do + dispatch = make_dispatch("/v1/{name}") + + assert {:ok, Handler, [], %{name: "messages"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/messages") + end + + test "with nested bindings" do + dispatch = make_dispatch("/v1/{message.name}") + + assert {:ok, Handler, [], %{"message.name": "messages"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/messages") + end + + test "with multiple bindings" do + dispatch = make_dispatch("/v1/users/{user_id}/messages/{message.message_id}") + + assert {:ok, Handler, [], %{user_id: "1", "message.message_id": "2"}, :undefined, + :undefined} == + Router.match(dispatch, "localhost", "/v1/users/1/messages/2") + end + + test "with multiple sequential bindings" do + dispatch = make_dispatch("/v1/{a}/{b}/{c}") + + assert {:ok, Handler, [], %{a: "a", b: "b", c: "c"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/a/b/c") + end + + test "with any " do + dispatch = make_dispatch("/*") + + assert {:ok, Handler, [], %{}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1") + end + + test "with 'any' assignment" do + dispatch = make_dispatch("/{a=*}") + + assert {:ok, Handler, [], %{a: "v1"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1") + end + + test "with 'catch all' assignment" do + dispatch = make_dispatch("/{a=**}") + + assert {:ok, Handler, [], %{a: "v1/messages"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/messages") + end + + test "with 'any' and 'catch all'" do + dispatch = make_dispatch("/*/**") + + assert {:ok, Handler, [], %{}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/foo/bar/baz") + end + + test "with 'any' and 'catch all' assignment" do + dispatch = make_dispatch("/*/a/{b=c/*}/d/{e=**}") + + assert {:ok, Handler, [], %{b: "c/foo", e: "bar/baz/biz"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/a/c/foo/d/bar/baz/biz") + end + + test "with complex binding" do + dispatch = make_dispatch("/v1/{name=messages}") + + assert {:ok, Handler, [], %{name: "messages"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/messages") + + assert {:error, :notfound, :path} == + Router.match(dispatch, "localhost", "/v1/should_not_match") + end + + test "with complex binding and 'any'" do + dispatch = make_dispatch("/v1/{name=messages/*}") + + assert {:ok, Handler, [], %{name: "messages/12345"}, :undefined, :undefined} == + Router.match(dispatch, "localhost", "/v1/messages/12345") + + assert {:error, :notfound, :path} == + Router.match(dispatch, "localhost", "/v1/should_not_match/12345") + end + + test "with complex binding, wildcards and trailing route" do + dispatch = make_dispatch("/v1/{name=shelves/*/books/*}") + + assert {:ok, Handler, [], %{name: "shelves/example-shelf/books/example-book"}, :undefined, + :undefined} == + Router.match(dispatch, "localhost", "/v1/shelves/example-shelf/books/example-book") + + assert {:error, :notfound, :path} == + Router.match(dispatch, "localhost", "/v1/shelves/example-shelf/not_books") + end + + test "with complex binding, wildcards and suffix" do + dispatch = make_dispatch("/v1/{name=shelves/*/books/*}/suffix") + + assert {:ok, Handler, [], %{name: "shelves/example-shelf/books/example-book"}, :undefined, + :undefined} == + Router.match( + dispatch, + "localhost", + "/v1/shelves/example-shelf/books/example-book/suffix" + ) + + assert {:error, :notfound, :path} == + Router.match( + dispatch, + "localhost", + "/v1/shelves/example-shelf/books/example-book/another_suffix" + ) + end + + test "with mixed complex binding" do + dispatch = make_dispatch("/v1/{a=users/*}/messages/{message_id}/{c=books/*}") + + assert {:ok, Handler, [], %{a: "users/foobar", message_id: "1", c: "books/barbaz"}, + :undefined, + :undefined} == + Router.match(dispatch, "localhost", "/v1/users/foobar/messages/1/books/barbaz") + + assert {:error, :notfound, :path} == + Router.match(dispatch, "localhost", "/v1/users/1/books/barbaz") + end + + test "with mixed sequential complex binding" do + dispatch = make_dispatch("/v1/{a=users/*}/{b=messages}/{c=books/*}") + + assert {:ok, Handler, [], %{a: "users/foobar", b: "messages", c: "books/barbaz"}, + :undefined, + :undefined} == + Router.match(dispatch, "localhost", "/v1/users/foobar/messages/books/barbaz") + + assert {:error, :notfound, :path} == + Router.match(dispatch, "localhost", "/v1/users/foobar/messages/book/books/barbaz") + end + end + + defp make_dispatch(path) do + {_method, _, match} = GRPC.Server.Router.build_route(path) + + [ + {:_, [], + [ + {match, [], Handler, []} + ]} + ] + end +end diff --git a/test/grpc/server/adapters/cowboy_test.exs b/test/grpc/server/adapters/cowboy_test.exs new file mode 100644 index 000000000..3877dafcf --- /dev/null +++ b/test/grpc/server/adapters/cowboy_test.exs @@ -0,0 +1,52 @@ +defmodule GRPC.Server.Adapters.CowboyTest do + use ExUnit.Case, async: false + + alias GRPC.Server.Adapters.Cowboy + + describe "child_spec/4" do + test "produces the correct socket opts for ranch_tcp for inet" do + spec = + Cowboy.child_spec(:endpoint, [], 8080, [ + {:foo, :bar}, + {:ip, {127, 0, 0, 1}}, + {:ipv6_v6only, false}, + {:net, :inet}, + {:baz, :foo} + ]) + + socket_opts = get_socket_opts_from_child_spec(spec) + + assert Enum.sort(socket_opts) == + Enum.sort([:inet, {:ip, {127, 0, 0, 1}}, {:ipv6_v6only, false}, {:port, 8080}]) + end + + test "produces the correct socket opts for ranch_tcp for inet6" do + spec = + Cowboy.child_spec(:endpoint, [], 8081, [ + {:foo, :bar}, + {:ip, {0, 0, 0, 0, 0, 0, 0, 1}}, + {:ipv6_v6only, true}, + {:net, :inet6}, + {:baz, :foo} + ]) + + socket_opts = get_socket_opts_from_child_spec(spec) + + assert Enum.sort(socket_opts) == + Enum.sort([ + :inet6, + {:ip, {0, 0, 0, 0, 0, 0, 0, 1}}, + {:ipv6_v6only, true}, + {:port, 8081} + ]) + end + end + + defp get_socket_opts_from_child_spec(spec) do + {_Cowboy, _start_link, start_opts} = spec.start + [_http, _endpoint, _empty_list, ranch_listener_call] = start_opts + {_ranch_listener_sup, _start_link, ranch_listener_opts} = ranch_listener_call + [_endpoint, _ranch_tcp, transport_opts, _cowboy_clear, _opts_map] = ranch_listener_opts + transport_opts.socket_opts + end +end diff --git a/test/grpc/server/adapters/report_exception_test.exs b/test/grpc/server/adapters/report_exception_test.exs new file mode 100644 index 000000000..759b6f5cf --- /dev/null +++ b/test/grpc/server/adapters/report_exception_test.exs @@ -0,0 +1,91 @@ +defmodule ExceptionServer do + use GenServer + + # Callbacks + + @impl true + def init(pid) do + {:ok, pid} + end + + @impl true + def handle_cast(:case_boom, state) do + a = fn -> :ok end + + case a.() do + :error -> :boom + end + + {:noreply, state} + end + + @impl true + def handle_cast(:bad_arg_boom, state) do + ets = :ets.new(:foo, []) + :ets.delete(ets) + :ets.insert(ets, 1) + + {:noreply, state} + end + + @impl true + def terminate(reason, state) do + send(state, {:boom, reason}) + :ok + end +end + +defmodule GRPC.Server.Adapters.ReportExceptionTest do + use ExUnit.Case, async: true + alias GRPC.Server.Adapters.ReportException + + describe "new/3" do + test "with runtime error" do + assert %GRPC.Server.Adapters.ReportException{ + __exception__: true, + adapter_extra: [req: :ok], + kind: :error, + reason: %RuntimeError{message: "hi", __exception__: true}, + stack: [] + } == ReportException.new([req: :ok], RuntimeError.exception("hi")) + end + + test "with case clause error" do + {:ok, pid} = GenServer.start_link(ExceptionServer, self()) + + GenServer.cast(pid, :case_boom) + + receive do + {:boom, {_reason, stack} = err} -> + assert %GRPC.Server.Adapters.ReportException{ + __exception__: true, + adapter_extra: [req: :ok], + kind: :error, + reason: %CaseClauseError{term: :ok}, + stack: stack + } == ReportException.new([{:req, :ok}], err) + end + end + + test "with badarg error" do + {:ok, pid} = GenServer.start_link(ExceptionServer, self()) + + GenServer.cast(pid, :bad_arg_boom) + + receive do + {:boom, {_reason, stack} = err} -> + assert %GRPC.Server.Adapters.ReportException{ + __exception__: true, + adapter_extra: [req: :ok], + kind: :error, + reason: %ArgumentError{ + __exception__: true, + message: + "errors were found at the given arguments:\n\n * 1st argument: the table identifier does not refer to an existing ETS table\n * 2nd argument: not a tuple\n" + }, + stack: stack + } == ReportException.new([{:req, :ok}], err) + end + end + end +end diff --git a/test/grpc/server/interceptors/cors_test.exs b/test/grpc/server/interceptors/cors_test.exs new file mode 100644 index 000000000..7805ab7b5 --- /dev/null +++ b/test/grpc/server/interceptors/cors_test.exs @@ -0,0 +1,314 @@ +defmodule GRPC.Server.Interceptors.CORSTest.Endpoint.FunctionCapture do + use GRPC.Endpoint + + intercept(GRPC.Server.Interceptors.CORS, + allow_origin: &GRPC.Server.Interceptors.CORSTest.allow_origin/2, + allow_headers: &GRPC.Server.Interceptors.CORSTest.allow_headers/2 + ) +end + +defmodule GRPC.Server.Interceptors.CORSTest.Endpoint.BinaryConcatenation do + use GRPC.Endpoint + + origin1 = "https://subdomain1.domain.com" + origin2 = "https://subdomain2.domain.com" + + intercept( + GRPC.Server.Interceptors.CORS, + allow_origin: origin1 <> "," <> origin2, + allow_headers: "MySpecialHeader,AndAnother" + ) +end + +defmodule GRPC.Server.Interceptors.CORSTest do + use ExUnit.Case, async: false + + alias GRPC.Server.Interceptors.CORS, as: CORSInterceptor + alias GRPC.Server.Stream + + defmodule FakeRequest do + defstruct [] + end + + @server_name :server + @rpc {1, 2, 3} + @adaptor GRPC.Test.ServerAdapter + @function_header_value "from-function" + @default_http_headers %{ + "accept" => "application/grpc-web-text", + "accept-encoding" => "gzip, deflate, br, zstd", + "accept-language" => "en-US,en;q=0.5", + "connection" => "keep-alive", + "content-length" => "20", + "content-type" => "application/grpc-web-text", + "dnt" => "1", + "host" => "http://myhost:4100", + "priority" => "u=0", + "referer" => "http://localhost:3000/", + "sec-fetch-dest" => "empty", + "sec-fetch-mode" => "cors", + "sec-fetch-site" => "same-site", + "user-agent" => "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0", + "x-grpc-web" => "1", + "x-user-agent" => "grpc-web-javascript/0.1" + } + @requested_allowed_headers "Authorized" + @custom_allowed_headers "MySpecialHeader,AndAnother" + + def allow_origin(_req, _stream), do: @function_header_value + def allow_headers(_req, _stream), do: @custom_allowed_headers + + def create_stream() do + %Stream{ + adapter: @adaptor, + server: @server_name, + rpc: @rpc, + http_request_headers: @default_http_headers + } + end + + test "Sends headers CORS for for http transcoding and grpcweb requests" do + request = %FakeRequest{} + stream = create_stream() + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :http_transcode}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + assert_received({:setting_headers, _headers}, "Failed to set CORS headers during grpcweb") + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + assert_received({:setting_headers, _headers}, "Failed to set CORS headers during grpcweb") + end + + test "Does not send CORS headers for normal grpc requests" do + request = %FakeRequest{} + stream = create_stream() + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpc}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + refute_received({:setting_headers, _headers}, "Set CORS headers during grpc") + end + + test "CORS allow origin header value is configuraable with a static string" do + request = %FakeRequest{} + stream = Map.put(create_stream(), :access_mode, :grpcweb) + domain = "https://mydomain.io" + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: domain) + ) + + assert_received( + {:setting_headers, %{"access-control-allow-origin" => ^domain}}, + "Incorrect static header" + ) + end + + test "CORS allow origin init does not accept non-string arguments" do + assert_raise(ArgumentError, fn -> CORSInterceptor.init(allow_origin: :atom) end) + assert_raise(ArgumentError, fn -> CORSInterceptor.init(allow_origin: 1) end) + assert_raise(ArgumentError, fn -> CORSInterceptor.init(allow_origin: 1.0) end) + assert_raise(ArgumentError, fn -> CORSInterceptor.init(allow_origin: []) end) + assert_raise(ArgumentError, fn -> CORSInterceptor.init(allow_origin: %{}) end) + end + + test "CORS allow origin header value is configuraable with a two-arity function" do + request = %FakeRequest{} + stream = Map.put(create_stream(), :access_mode, :grpcweb) + + # fetch the interceptor state from the fake endpoint + [{_interceptor, interceptor_state}] = + GRPC.Server.Interceptors.CORSTest.Endpoint.FunctionCapture.__meta__(:interceptors) + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + interceptor_state + ) + + assert_received( + {:setting_headers, %{"access-control-allow-origin" => @function_header_value}}, + "Incorrect header when using function" + ) + end + + test "CORS allow origin header value is configuraable with binary concatenation" do + request = %FakeRequest{} + stream = Map.put(create_stream(), :access_mode, :grpcweb) + + # fetch the interceptor state from the fake endpoint + [{_interceptor, interceptor_state}] = + GRPC.Server.Interceptors.CORSTest.Endpoint.BinaryConcatenation.__meta__(:interceptors) + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + interceptor_state + ) + + assert_received( + {:setting_headers, + %{ + "access-control-allow-origin" => + "https://subdomain1.domain.com,https://subdomain2.domain.com" + }}, + "Incorrect header when using function" + ) + end + + test "CORS Access-Control-Allowed-Headers is included in response when clients request it" do + request = %FakeRequest{} + + stream = %{ + create_stream() + | access_mode: :grpcweb, + http_request_headers: + Map.put( + @default_http_headers, + "access-control-request-headers", + @requested_allowed_headers + ) + } + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + assert_received( + {:setting_headers, %{"access-control-allow-headers" => @requested_allowed_headers}}, + "Incorrect header when using function" + ) + end + + test "CORS Access-Control-Allowed-Headers is configurable with a static string" do + request = %FakeRequest{} + + stream = %{ + create_stream() + | access_mode: :grpcweb, + http_request_headers: + Map.put( + @default_http_headers, + "access-control-request-headers", + @requested_allowed_headers + ) + } + + allowed_headers = "Test" + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*", allow_headers: allowed_headers) + ) + + assert_received( + {:setting_headers, %{"access-control-allow-headers" => ^allowed_headers}}, + "Incorrect header when using function" + ) + end + + test "CORS Access-Control-Allowed-Headers is configurable with a two-arity function" do + request = %FakeRequest{} + + stream = %{ + create_stream() + | access_mode: :grpcweb, + http_request_headers: + Map.put( + @default_http_headers, + "access-control-request-headers", + @requested_allowed_headers + ) + } + + # fetch the interceptor state from the fake endpoint + [{_interceptor, interceptor_state}] = + GRPC.Server.Interceptors.CORSTest.Endpoint.FunctionCapture.__meta__(:interceptors) + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + interceptor_state + ) + + assert_received( + {:setting_headers, %{"access-control-allow-headers" => @custom_allowed_headers}}, + "Incorrect header when using function" + ) + end + + test "CORS only on cors sec-fetch-mode" do + request = %FakeRequest{} + + stream = %{ + create_stream() + | access_mode: :grpcweb, + http_request_headers: Map.put(@default_http_headers, "sec-fetch-mode", "same-origin") + } + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + refute_received({:setting_headers, _}, "Set CORS header") + end + + test "No CORS if missing sec-fetch-mode header" do + request = %FakeRequest{} + + stream = %{ + create_stream() + | access_mode: :grpcweb, + http_request_headers: Map.delete(@default_http_headers, "sec-fetch-mode") + } + + {:ok, :ok} = + CORSInterceptor.call( + request, + %{stream | access_mode: :grpcweb}, + fn _request, _stream -> {:ok, :ok} end, + CORSInterceptor.init(allow_origin: "*") + ) + + refute_received({:setting_headers, _}, "Set CORS header") + end +end diff --git a/test/grpc/server/interceptors/logger_test.exs b/test/grpc/server/interceptors/logger_test.exs new file mode 100644 index 000000000..a05b23b34 --- /dev/null +++ b/test/grpc/server/interceptors/logger_test.exs @@ -0,0 +1,107 @@ +defmodule GRPC.Server.Interceptors.LoggerTest do + use ExUnit.Case, async: false + + import ExUnit.CaptureLog + + alias GRPC.Server.Interceptors.Logger, as: LoggerInterceptor + alias GRPC.Server.Stream + + defmodule FakeRequest do + defstruct [] + end + + @server_name :server + @rpc {1, 2, 3} + + setup do + log_level = Logger.level() + on_exit(fn -> Logger.configure(level: log_level) end) + end + + test "request id is only set if not previously set" do + assert Logger.metadata() == [] + + request_id = to_string(System.monotonic_time()) + request = %FakeRequest{} + stream = %Stream{server: @server_name, rpc: @rpc, request_id: request_id} + + LoggerInterceptor.call( + request, + stream, + fn ^request, ^stream -> {:ok, :ok} end, + LoggerInterceptor.init(level: :info) + ) + + assert [request_id: request_id] == Logger.metadata() + + stream = %{stream | request_id: nil} + + LoggerInterceptor.call( + :request, + stream, + fn :request, ^stream -> {:ok, :ok} end, + LoggerInterceptor.init(level: :info) + ) + + assert request_id == Logger.metadata()[:request_id] + end + + test "logs info-level by default" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{server: @server_name, rpc: @rpc, request_id: nil} + next = fn _stream, _request -> {:ok, :ok} end + opts = LoggerInterceptor.init([]) + + logs = + capture_log(fn -> + LoggerInterceptor.call(request, stream, next, opts) + end) + + assert logs =~ ~r/\[info\]\s+Handled by #{inspect(@server_name)}/ + end + + test "allows customizing log level" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{server: @server_name, rpc: @rpc, request_id: nil} + next = fn _stream, _request -> {:ok, :ok} end + opts = LoggerInterceptor.init(level: :warning) + + logs = + capture_log(fn -> + LoggerInterceptor.call(request, stream, next, opts) + end) + + assert logs =~ ~r/\[warn(?:ing)?\]\s+Handled by #{inspect(@server_name)}/ + end + + @tag capture_log: true + test "calls next when above :logger level" do + Logger.configure(level: :all) + + request = %FakeRequest{} + stream = %Stream{server: @server_name, rpc: @rpc, request_id: nil} + next = fn stream, req -> send(self(), {:next_called, stream, req}) end + opts = LoggerInterceptor.init(level: :info) + + LoggerInterceptor.call(request, stream, next, opts) + + assert_receive {:next_called, ^request, ^stream} + end + + test "calls next when below :logger level" do + Logger.configure(level: :warning) + + request = %FakeRequest{} + stream = %Stream{server: @server_name, rpc: @rpc, request_id: nil} + next = fn stream, req -> send(self(), {:next_called, stream, req}) end + opts = LoggerInterceptor.init(level: :info) + + LoggerInterceptor.call(request, stream, next, opts) + + assert_receive {:next_called, ^request, ^stream} + end +end diff --git a/test/grpc/server/router/query_test.exs b/test/grpc/server/router/query_test.exs new file mode 100644 index 000000000..ffa4d0ca6 --- /dev/null +++ b/test/grpc/server/router/query_test.exs @@ -0,0 +1,30 @@ +defmodule GRPC.Server.Router.QueryTest do + use ExUnit.Case, async: true + alias GRPC.Server.Router.Query + + test "`a=b&c=d` should be decoded as a map" do + assert %{"a" => "b", "c" => "d"} == Query.decode("a=b&c=d") + end + + test "`param=A¶m=B` should be decoded as a list" do + assert %{"param" => ["A", "B"]} == Query.decode("param=A¶m=B") + end + + test "`root.a=A&root.b=B` should be decoded as a nested map" do + assert %{"root" => %{"a" => "A", "b" => "B"}} == Query.decode("root.a=A&root.b=B") + end + + test "`root.a=A&root.a=B` should be decoded as a nested map with a list" do + assert %{"root" => %{"a" => ["A", "B"]}} == Query.decode("root.a=A&root.a=B") + end + + test "deeply nested map should be decoded" do + assert %{"root" => %{"a" => %{"b" => %{"c" => %{"d" => "A"}}}, "b" => "B"}, "c" => "C"} == + Query.decode("root.a.b.c.d=A&root.b=B&c=C") + end + + test "pairs without value are decoded as `\"\"`" do + assert %{"param" => "", "a" => "A"} == + Query.decode("param=&a=A") + end +end diff --git a/test/grpc/server/router/template_test.exs b/test/grpc/server/router/template_test.exs new file mode 100644 index 000000000..f19b9cd20 --- /dev/null +++ b/test/grpc/server/router/template_test.exs @@ -0,0 +1,175 @@ +defmodule GRPC.Server.Router.TemplateTest do + use ExUnit.Case, async: true + alias GRPC.Server.Router.Template + + describe "tokenize/2" do + test "can tokenize simple paths" do + assert [{:/, []}] = Template.tokenize("/") + + assert [{:/, []}, {:identifier, "v1", []}, {:/, []}, {:identifier, "messages", []}] = + Template.tokenize("/v1/messages") + end + + test "can tokenize simple paths with wildcards" do + assert [ + {:/, []}, + {:identifier, "v1", []}, + {:/, []}, + {:identifier, "messages", []}, + {:/, []}, + {:*, []} + ] == Template.tokenize("/v1/messages/*") + end + + test "can tokenize simple variables" do + assert [ + {:/, []}, + {:identifier, "v1", []}, + {:/, []}, + {:identifier, "messages", []}, + {:/, []}, + {:"{", []}, + {:identifier, "message_id", []}, + {:"}", []} + ] == Template.tokenize("/v1/messages/{message_id}") + end + + test "can tokenize variable assignments in bindings" do + assert [ + {:/, []}, + {:identifier, "v1", []}, + {:/, []}, + {:"{", []}, + {:identifier, "name", []}, + {:=, []}, + {:identifier, "messages", []}, + {:"}", []} + ] == Template.tokenize("/v1/{name=messages}") + end + + test "can tokenize variable sub-paths in bindings" do + assert [ + {:/, []}, + {:identifier, "v1", []}, + {:/, []}, + {:"{", []}, + {:identifier, "name", []}, + {:=, []}, + {:identifier, "messages", []}, + {:/, []}, + {:*, []}, + {:"}", []} + ] == Template.tokenize("/v1/{name=messages/*}") + end + + test "can tokenize field paths in bindings" do + assert [ + {:/, []}, + {:identifier, "v1", []}, + {:/, []}, + {:identifier, "messages", []}, + {:/, []}, + {:"{", []}, + {:identifier, "message_id", []}, + {:"}", []}, + {:/, []}, + {:"{", []}, + {:identifier, "sub.subfield", []}, + {:"}", []} + ] == Template.tokenize("/v1/messages/{message_id}/{sub.subfield}") + end + + test "can tokenize single wildcard" do + assert [{:/, []}, {:*, []}] == Template.tokenize("/*") + end + + test "can tokenize multiple wildcards" do + assert [ + {:/, []}, + {:*, []}, + {:/, []}, + {:*, []}, + {:*, []} + ] == Template.tokenize("/*/**") + end + end + + describe "parse/3" do + test "can parse simple paths" do + assert [] == + "/" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse paths with identifiers" do + assert ["v1", "messages"] == + "/v1/messages" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse paths with 'any'" do + assert ["v1", "messages", {:_, []}] == + "/v1/messages/*" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse paths with 'catch all'" do + assert ["v1", "messages", {:__, []}] == + "/v1/messages/**" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse simple bindings with variables" do + assert ["v1", "messages", {:message_id, []}] == + "/v1/messages/{message_id}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse bindings with variable assignment" do + assert ["v1", {:name, ["messages", {:_, []}]}] == + "/v1/{name=messages/*}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse bindings with variable assignment to any" do + assert ["v1", {:name, [{:_, []}]}] == + "/v1/{name=*}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse multiple bindings with variable assignment" do + assert ["v1", {:name, ["messages"]}, {:message_id, []}] == + "/v1/{name=messages}/{message_id}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse bindings with field paths" do + assert ["v1", "messages", {:"sub.subfield", []}] == + "/v1/messages/{sub.subfield}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "supports deeper nested field path " do + assert ["v1", "messages", {:"sub.nested.nested.nested", []}] == + "/v1/messages/{sub.nested.nested.nested}" + |> Template.tokenize() + |> Template.parse([]) + end + + test "can parse multiple-bindings with field paths " do + assert ["v1", "messages", {:"first.subfield", []}, {:"second.subfield", []}] == + "/v1/messages/{first.subfield}/{second.subfield}" + |> Template.tokenize() + |> Template.parse([]) + end + end +end diff --git a/test/grpc/server/router_test.exs b/test/grpc/server/router_test.exs new file mode 100644 index 000000000..f67183099 --- /dev/null +++ b/test/grpc/server/router_test.exs @@ -0,0 +1,161 @@ +defmodule GRPC.Server.RouterTest do + use ExUnit.Case, async: true + alias GRPC.Server.Router + + describe "build_route/1" do + test "returns a route with {http_method, path, match} based on the template string" do + path = "/v1/messages/{message_id}" + + assert {:get, ^path, match} = Router.build_route(:get, "/v1/messages/{message_id}") + assert ["v1", "messages", {:message_id, []}] = match + end + + test "defaults to setting method to `:post` if no method was provided" do + path = "/transcode.Messaging/GetMessage" + + assert {:post, ^path, match} = Router.build_route(path) + assert ["transcode.Messaging", "GetMessage"] = match + end + + test "returns a route with {http_method, path, match} based HttRule" do + path = "/v1/messages/{message_id}" + rule = build_simple_rule(:get, "/v1/messages/{message_id}") + + assert {:get, ^path, match} = Router.build_route(rule) + assert ["v1", "messages", {:message_id, []}] = match + end + end + + describe "match/3" do + test "with no segments" do + assert {true, %{}} = Router.match("/", []) + end + + test "with segments and no matchers" do + refute Router.match("/foo", []) + end + + test "with no bindings" do + {_, _, match} = Router.build_route("/transcode.Messaging/GetMessage") + + assert {true, %{}} == Router.match("/transcode.Messaging/GetMessage", match) + assert false == Router.match("/transcode.Messaging/GetMessages", match) + end + + test "with simple bindings" do + {_, _, match} = Router.build_route(:get, "/v1/{name}") + + assert {true, %{name: "messages"}} == Router.match("/v1/messages", match) + end + + test "with nested bindings" do + {_, _, match} = Router.build_route(:get, "/v1/{message.name}") + + assert {true, %{"message.name": "messages"}} == Router.match("/v1/messages", match) + end + + test "with multiple bindings" do + {_, _, match} = + Router.build_route(:get, "/v1/users/{user_id}/messages/{message.message_id}") + + assert {true, %{user_id: "1", "message.message_id": "2"}} == + Router.match("/v1/users/1/messages/2", match) + end + + test "with multiple sequential bindings" do + {_, _, match} = Router.build_route("/v1/{a}/{b}/{c}") + + assert {true, %{a: "a", b: "b", c: "c"}} == Router.match("/v1/a/b/c", match) + end + + test "with 'any'" do + {_, _, match} = Router.build_route("/*") + + assert {true, %{}} == Router.match("/v1", match) + end + + test "with 'any' assignment" do + {_, _, match} = Router.build_route("/{a=*}") + + assert {true, %{a: "v1"}} == Router.match("/v1", match) + end + + test "with 'catch all' assignment" do + {_, _, match} = Router.build_route("/{a=**}") + + assert {true, %{a: "v1/messages"}} == Router.match("/v1/messages", match) + end + + test "with 'any' and 'catch all'" do + {_, _, match} = Router.build_route("/*/**") + assert {true, %{}} == Router.match("/v1/foo/bar/baz", match) + end + + test "with 'any' and 'catch all' assignment" do + {_, _, match} = Router.build_route("/*/a/{b=c/*}/d/{e=**}") + + assert {true, %{b: "c/foo", e: "bar/baz/biz"}} == + Router.match("/v1/a/c/foo/d/bar/baz/biz", match) + end + + test "with complex binding" do + {_, _, match} = Router.build_route("/v1/{name=messages}") + + assert {true, %{name: "messages"}} == Router.match("/v1/messages", match) + refute Router.match("/v1/should_not_match", match) + end + + test "with complex binding and 'any'" do + {_, _, match} = Router.build_route("/v1/{name=messages/*}") + + assert {true, %{name: "messages/12345"}} == Router.match("/v1/messages/12345", match) + refute Router.match("/v1/should_not_match/12345", match) + end + + test "with complex binding, wildcards and trailing route" do + {_, _, match} = Router.build_route("/v1/{name=shelves/*/books/*}") + + assert {true, %{name: "shelves/example-shelf/books/example-book"}} == + Router.match("/v1/shelves/example-shelf/books/example-book", match) + + refute Router.match("/v1/shelves/example-shelf/not_books", match) + end + + test "with complex binding, wildcards and suffix" do + {_, _, match} = Router.build_route("/v1/{name=shelves/*/books/*}/suffix") + + assert {true, %{name: "shelves/example-shelf/books/example-book"}} == + Router.match( + "/v1/shelves/example-shelf/books/example-book/suffix", + match + ) + + refute Router.match( + "/v1/shelves/example-shelf/books/example-book/another_suffix", + match + ) + end + + test "with mixed complex binding" do + {_, _, match} = Router.build_route("/v1/{a=users/*}/messages/{message_id}/{c=books/*}") + + assert {true, %{a: "users/foobar", message_id: "1", c: "books/barbaz"}} == + Router.match("/v1/users/foobar/messages/1/books/barbaz", match) + + assert false == Router.match("/v1/users/1/books/barbaz", match) + end + + test "with mixed sequential complex binding" do + {_, _, match} = Router.build_route("/v1/{a=users/*}/{b=messages}/{c=books/*}") + + assert {true, %{a: "users/foobar", b: "messages", c: "books/barbaz"}} == + Router.match("/v1/users/foobar/messages/books/barbaz", match) + + refute Router.match("/v1/users/foobar/messages/book/books/barbaz", match) + end + end + + defp build_simple_rule(method, pattern) do + %Google.Api.HttpRule{pattern: {method, pattern}} + end +end diff --git a/test/grpc/server/supervisor_test.exs b/test/grpc/server/supervisor_test.exs new file mode 100644 index 000000000..495b6b0dc --- /dev/null +++ b/test/grpc/server/supervisor_test.exs @@ -0,0 +1,44 @@ +defmodule GRPC.Server.SupervisorTest do + use ExUnit.Case, async: false + + alias GRPC.Server.Supervisor + + defmodule MockEndpoint do + def __meta__(_), do: [FeatureServer] + end + + describe "init/1" do + test "does not start children if opts sets false" do + assert {:ok, {%{strategy: :one_for_one}, []}} = + Supervisor.init(endpoint: MockEndpoint, port: 1234, start_server: false) + end + + test "fails if a tuple is passed" do + assert_raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure.", + fn -> + Supervisor.init({MockEndpoint, 1234}) + end + + assert_raise ArgumentError, + "passing a tuple as configuration for GRPC.Server.Supervisor is no longer supported. See the documentation for more information on how to configure.", + fn -> + Supervisor.init({MockEndpoint, 1234, start_server: true}) + end + end + + test "starts children if opts sets true" do + endpoint_str = "#{Macro.to_string(MockEndpoint)}" + + assert {:ok, + {%{strategy: :one_for_one}, + [ + %{ + id: {:ranch_listener_sup, ^endpoint_str}, + start: _, + type: :supervisor + } + ]}} = Supervisor.init(endpoint: MockEndpoint, port: 1234, start_server: true) + end + end +end diff --git a/test/grpc/server/transcode_test.exs b/test/grpc/server/transcode_test.exs new file mode 100644 index 000000000..61995a85a --- /dev/null +++ b/test/grpc/server/transcode_test.exs @@ -0,0 +1,63 @@ +defmodule GRPC.TranscodeTest do + use ExUnit.Case, async: true + alias GRPC.Server.Transcode + + test "map_request/5 with HttpRule.body: '*'" do + rule = %Google.Api.HttpRule{body: "*"} + request_body = %{"latitude" => 1, "longitude" => 2} + bindings = %{} + qs = "latitude=10&longitude=20" + + assert {:ok, %Routeguide.Point{latitude: 1, longitude: 2}} = + Transcode.map_request(rule, request_body, bindings, qs, Routeguide.Point) + end + + test "map_request/5 with empty HttpRule.body" do + rule = %Google.Api.HttpRule{body: ""} + request_body = %{"latitude" => 10, "longitude" => 20} + bindings = %{"latitude" => 5} + qs = "longitude=6" + + assert {:ok, %Routeguide.Point{latitude: 5, longitude: 6}} = + Transcode.map_request(rule, request_body, bindings, qs, Routeguide.Point) + end + + test "map_request/2 with HttpRule.body: " do + rule = %Google.Api.HttpRule{body: "location"} + request_body = %{"latitude" => 1, "longitude" => 2} + bindings = %{"name" => "test"} + + assert {:ok, %Routeguide.Feature{name: "test", location: point}} = + Transcode.map_request(rule, request_body, bindings, "", Routeguide.Feature) + + assert point.latitude == 1 + assert point.longitude == 2 + end + + test "map_response_body/2 with empty HttpRule.response_body" do + rule = %Google.Api.HttpRule{response_body: ""} + request_body = %{message: %{a: "b"}} + + assert request_body == Transcode.map_response_body(rule, request_body) + end + + test "map_response_body/2 with HttpRule.response_body: " do + rule = %Google.Api.HttpRule{response_body: "message"} + request_body = %{message: %{a: "b"}} + + assert %{a: "b"} == Transcode.map_response_body(rule, request_body) + end + + test "map_route_bindings/2 should stringify the keys" do + path_binding_atom = %{foo: "bar"} + path_binding_string = %{foo: "bar"} + + assert %{"foo" => "bar"} == Transcode.map_path_bindings(path_binding_atom) + assert %{"foo" => "bar"} == Transcode.map_path_bindings(path_binding_string) + end + + test "map_route_bindings/2 with '.' delimited identifiers should create a nested map" do + path_binding = %{"foo.bar.baz" => "biz"} + assert %{"foo" => %{"bar" => %{"baz" => "biz"}}} == Transcode.map_path_bindings(path_binding) + end +end diff --git a/test/grpc/status_test.exs b/test/grpc/status_test.exs new file mode 100644 index 000000000..0074a81d8 --- /dev/null +++ b/test/grpc/status_test.exs @@ -0,0 +1,50 @@ +defmodule GRPC.StatusTest do + use GRPC.DataCase, async: true + + alias GRPC.Status + + @status_codes_metadata [ + {:ok, 0, nil}, + {:cancelled, 1, "The operation was cancelled (typically by the caller)"}, + {:unknown, 2, "Unknown error"}, + {:invalid_argument, 3, "Client specified an invalid argument"}, + {:deadline_exceeded, 4, "Deadline expired before operation could complete"}, + {:not_found, 5, "Some requested entity (e.g., file or directory) was not found"}, + {:already_exists, 6, + "Some entity that we attempted to create (e.g., file or directory) already exists"}, + {:permission_denied, 7, + "The caller does not have permission to execute the specified operation"}, + {:resource_exhausted, 8, "Some resource has been exhausted"}, + {:failed_precondition, 9, + "Operation was rejected because the system is not in a state required for the operation's execution"}, + {:aborted, 10, "The operation was aborted"}, + {:out_of_range, 11, "Operation was attempted past the valid range"}, + {:unimplemented, 12, "Operation is not implemented or not supported/enabled in this service"}, + {:internal, 13, "Internal errors"}, + {:unavailable, 14, "The service is currently unavailable"}, + {:data_loss, 15, "Unrecoverable data loss or corruption"}, + {:unauthenticated, 16, + "The request does not have valid authentication credentials for the operation"} + ] + + for {function, expected_code, expected_message} <- @status_codes_metadata do + test "function #{function} returns expected code" do + assert unquote(expected_code) == Status.unquote(function)() + end + + test "code #{expected_code} returns the expected message" do + assert unquote(expected_message) == Status.status_message(unquote(expected_code)) + end + + test "code #{expected_code} returns the expected code_name" do + code_name = + case unquote(function) do + :ok -> "OK" + :cancelled -> "Canceled" + function -> function |> to_string() |> Macro.camelize() + end + + assert code_name == Status.code_name(unquote(expected_code)) + end + end +end diff --git a/test/grpc/stream_test.exs b/test/grpc/stream_test.exs new file mode 100644 index 000000000..eebfa6a95 --- /dev/null +++ b/test/grpc/stream_test.exs @@ -0,0 +1,267 @@ +defmodule GRPC.StreamTest do + use ExUnit.Case + doctest GRPC.Stream + + describe "simple test" do + defmodule TestInput do + defstruct [:message] + end + + defmodule FakeAdapter do + def get_headers(_), do: %{"content-type" => "application/grpc"} + end + + test "unary/2 creates a flow from a unary input" do + input = %TestInput{message: 1} + + result = + GRPC.Stream.unary(input) + |> GRPC.Stream.map(& &1) + |> GRPC.Stream.run() + + assert result == input + end + + test "unary/2 creates a flow with metadata" do + input = %TestInput{message: 1} + materializer = %GRPC.Server.Stream{adapter: FakeAdapter} + + flow = + GRPC.Stream.unary(input, materializer: materializer, propagate_context: true) + |> GRPC.Stream.map_with_context(fn meta, item -> + assert not is_nil(meta) + assert is_map(meta) + item + end) + + result = Enum.to_list(GRPC.Stream.to_flow(flow)) |> Enum.at(0) + assert result == input + end + + test "from/2 creates a flow from enumerable input" do + input = [%{message: "a"}, %{message: "b"}] + + flow = + GRPC.Stream.from(input, max_demand: 1) + |> GRPC.Stream.map(& &1) + + result = Enum.to_list(GRPC.Stream.to_flow(flow)) + assert result == input + end + + test "from_as_ctx/3 creates a flow from enumerable input" do + input = [%{message: "a"}, %{message: "b"}] + materializer = %GRPC.Server.Stream{adapter: FakeAdapter} + + flow = + GRPC.Stream.from(input, propagate_context: true, materializer: materializer) + |> GRPC.Stream.map_with_context(fn meta, item -> + assert not is_nil(meta) + assert is_map(meta) + item + end) + + result = Enum.to_list(GRPC.Stream.to_flow(flow)) + assert result == input + end + end + + describe "from/2" do + test "converts a list into a flow" do + stream = GRPC.Stream.from([1, 2, 3]) + assert %GRPC.Stream{} = stream + + result = stream |> GRPC.Stream.map(&(&1 * 2)) |> GRPC.Stream.to_flow() |> Enum.to_list() + assert Enum.sort(result) == [2, 4, 6] + end + end + + describe "ask/3 with pid" do + test "calls a pid and returns the response" do + pid = + spawn(fn -> + receive do + {:request, :hello, test_pid} -> + send(test_pid, {:response, :world}) + end + end) + + result = + GRPC.Stream.from([:hello]) + |> GRPC.Stream.ask(pid) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert result == [:world] + end + + test "returns error if pid not alive" do + pid = spawn(fn -> :ok end) + # wait for the process to exit + ref = Process.monitor(pid) + assert_receive {:DOWN, ^ref, _, _, _} + + result = + GRPC.Stream.from(["msg"]) + |> GRPC.Stream.ask(pid) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert result == [{:error, "msg", :not_alive}] + end + end + + describe "ask/3 with GenServer" do + defmodule TestServer do + use GenServer + + def start_link(_) do + GenServer.start_link(__MODULE__, nil, name: __MODULE__) + end + + def init(_), do: {:ok, %{}} + + def handle_info({:request, value, from}, state) do + Process.send(from, {:response, value}, []) + {:noreply, state} + end + end + + setup do + {:ok, _pid} = TestServer.start_link([]) + :ok + end + + test "asks GenServer and receives correct response" do + stream = GRPC.Stream.from(["abc"]) + + result = + stream + |> GRPC.Stream.ask(TestServer) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert result == ["abc"] + end + end + + describe "map/2, flat_map/2, filter/2" do + test "maps values correctly" do + result = + GRPC.Stream.from([1, 2, 3]) + |> GRPC.Stream.map(&(&1 * 10)) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert Enum.sort(result) == [10, 20, 30] + end + + test "flat_maps values correctly" do + result = + GRPC.Stream.from([1, 2]) + |> GRPC.Stream.flat_map(&[&1, &1]) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert Enum.sort(result) == [1, 1, 2, 2] + end + + test "filters values correctly" do + result = + GRPC.Stream.from([1, 2, 3, 4]) + |> GRPC.Stream.filter(&(rem(&1, 2) == 0)) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + + assert result == [2, 4] + end + end + + describe "test complex operations" do + test "pipeline with all GRPC.Stream operators" do + target = + spawn(fn -> + receive_loop() + end) + + input = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + result = + input + |> GRPC.Stream.from() + # 2..11 + |> GRPC.Stream.map(&(&1 + 1)) + # [2,4,3,6,4,8,...] + |> GRPC.Stream.flat_map(&[&1, &1 * 2]) + # keep evens + |> GRPC.Stream.filter(&(rem(&1, 2) == 0)) + + # remove duplicates + |> GRPC.Stream.uniq() + # multiply by 10 via process + |> GRPC.Stream.ask(target) + |> GRPC.Stream.partition() + |> GRPC.Stream.reduce(fn -> [] end, fn i, acc -> [i | acc] end) + |> GRPC.Stream.to_flow() + |> Enum.to_list() + |> List.flatten() + |> Enum.sort() + + assert result == [20, 40, 60, 80, 100, 120, 140, 160, 180, 200, 220] + end + end + + describe "join_with/merge streams" do + test "merges input stream with joined GenStage producer" do + defmodule TestProducer do + use GenStage + + def start_link(items) do + GenStage.start_link(__MODULE__, items) + end + + def init(items) do + {:producer, items} + end + + def handle_demand(demand, state) when demand > 0 do + {events, remaining} = Enum.split(state, demand) + + {:noreply, events, remaining} + end + end + + elements = Enum.to_list(4..1000) + {:ok, producer_pid} = TestProducer.start_link(elements) + + input = [1, 2, 3] + + task = + Task.async(fn -> + GRPC.Stream.from(input, join_with: producer_pid, max_demand: 500) + |> GRPC.Stream.map(fn it -> it end) + |> GRPC.Stream.run_with(%GRPC.Server.Stream{}, dry_run: true) + end) + + result = + case Task.yield(task, 1000) || Task.shutdown(task) do + {:ok, _} -> :ok + _ -> :ok + end + + if Process.alive?(producer_pid) do + Process.exit(producer_pid, :normal) + end + + assert result == :ok + end + end + + defp receive_loop do + receive do + {:request, item, from} -> + send(from, {:response, item * 10}) + receive_loop() + end + end +end diff --git a/test/grpc/transport/http2_test.exs b/test/grpc/transport/http2_test.exs index 624c37ef9..2c84cc671 100644 --- a/test/grpc/transport/http2_test.exs +++ b/test/grpc/transport/http2_test.exs @@ -1,30 +1,21 @@ defmodule GRPC.Transport.HTTP2Test do use ExUnit.Case, async: true - alias GRPC.Channel + alias GRPC.{Channel, Status} alias GRPC.Transport.HTTP2 - @channel %Channel{scheme: "http", host: "grpc.io"} alias GRPC.Client.Stream + alias GRPC.Server.Stream, as: ServerStream + + @channel %Channel{scheme: "http", host: "grpc.io"} defp assert_header({key, _v} = pair, headers) do assert pair == Enum.find(headers, nil, fn {k, _v} -> if k == key, do: true end) end - test "server_trailers/3 returns map without grpc-status-details-bin" do - trailers = HTTP2.server_trailers(GRPC.Status.invalid_argument(), "test message", "") + test "server_trailers/2 returns map with grpc-status and grpc-message" do + trailers = HTTP2.server_trailers(GRPC.Status.invalid_argument(), "test message") - assert trailers == %{"grpc-message" => "test message", "grpc-status" => "3"} - end - - test "server_trailers/3 returns map with grpc-status-details-bin" do - trailers = - HTTP2.server_trailers(GRPC.Status.invalid_argument(), "test message", "some details") - - assert trailers == %{ - "grpc-message" => "test message", - "grpc-status" => "3", - "grpc-status-details-bin" => Base.encode64("some details") - } + assert trailers == %{"grpc-message" => "test%20message", "grpc-status" => "3"} end test "client_headers/3 returns basic headers" do @@ -61,8 +52,9 @@ defmodule GRPC.Transport.HTTP2Test do headers = HTTP2.client_headers(stream, %{metadata: %{"key1-bin" => "abc", "key2-bin" => <<194, 128>>}}) + # Base64 encoded without padding (per gRPC spec) assert_header({"key1-bin", "YWJj"}, headers) - assert_header({"key2-bin", "woA"}, headers) + assert_header({"key2-bin", "woA="}, headers) end test "client_headers/3 rejects reserved headers in metadata" do @@ -116,4 +108,28 @@ defmodule GRPC.Transport.HTTP2Test do assert {_, "application/grpc+custom-codec"} = Enum.find(headers, fn {key, _} -> key == "content-type" end) end + + test "server_headers/3 sets content-type based on the codec name" do + for {expected_content_type, codec} <- [ + {"grpc-web-text", GRPC.Codec.WebText}, + {"grpc+erlpack", GRPC.Codec.Erlpack} + ] do + stream = %ServerStream{codec: codec} + + assert %{"content-type" => "application/" <> ^expected_content_type} = + HTTP2.server_headers(stream) + end + end + + test "decode_headers/2 url decodes grpc-message" do + trailers = HTTP2.server_trailers(Status.unknown(), "Unknown error") + assert %{"grpc-message" => "Unknown error"} = HTTP2.decode_headers(trailers) + end + + test "server_trailers/3 sets url encoded grpc-message" do + assert %{"grpc-message" => "Ok"} = HTTP2.server_trailers(Status.ok(), "Ok") + + assert %{"grpc-message" => "Unknown%20error"} = + HTTP2.server_trailers(Status.unknown(), "Unknown error") + end end diff --git a/test/grpc/transport/utils_test.exs b/test/grpc/transport/utils_test.exs index 3b39c860b..bcd6ce08a 100644 --- a/test/grpc/transport/utils_test.exs +++ b/test/grpc/transport/utils_test.exs @@ -93,9 +93,14 @@ defmodule GRPC.Transport.UtilsTest do test "encode_status_details/2 returns status with correct type_url" do status_details = encode_status_details(GRPC.Status.invalid_argument(), [ - Google.Rpc.QuotaFailure.new( - violations: [%{subject: "test", description: "Limit one greeting per person"}] - ) + %Google.Rpc.QuotaFailure{ + violations: [ + %Google.Rpc.QuotaFailure.Violation{ + subject: "test", + description: "Limit one greeting per person" + } + ] + } ]) %Google.Rpc.Status{code: 3, details: [%Google.Protobuf.Any{type_url: type_url}]} = @@ -106,12 +111,22 @@ defmodule GRPC.Transport.UtilsTest do test "decode_status_details/1 decodes status details succesfully" do details = [ - Google.Rpc.QuotaFailure.new( - violations: [%{subject: "test", description: "Limit one greeting per person"}] - ), - Google.Rpc.BadRequest.new( - violations: [%{description: "some description", field: "some field"}] - ) + %Google.Rpc.QuotaFailure{ + violations: [ + %Google.Rpc.QuotaFailure.Violation{ + subject: "test", + description: "Limit one greeting per person" + } + ] + }, + %Google.Rpc.BadRequest{ + field_violations: [ + %Google.Rpc.BadRequest.FieldViolation{ + description: "some description", + field: "some field" + } + ] + } ] encoded_status_details = encode_status_details(GRPC.Status.invalid_argument(), details) diff --git a/test/support/data_case.ex b/test/support/data_case.ex new file mode 100644 index 000000000..8e0de59d3 --- /dev/null +++ b/test/support/data_case.ex @@ -0,0 +1,9 @@ +defmodule GRPC.DataCase do + use ExUnit.CaseTemplate + + using do + quote do + import GRPC.Factory + end + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex new file mode 100644 index 000000000..5998d9464 --- /dev/null +++ b/test/support/factory.ex @@ -0,0 +1,85 @@ +defmodule GRPC.Factory do + @moduledoc false + + alias GRPC.Channel + alias GRPC.Credential + + def build(resource, attrs \\ %{}) do + name = :"#{resource}_factory" + + attrs = Map.new(attrs) + + data = + if function_exported?(__MODULE__, name, 1) do + apply(__MODULE__, name, [attrs]) + else + apply(__MODULE__, name, []) + end + + Map.merge(data, attrs) + end + + def channel_factory do + %Channel{ + host: "localhost", + port: 1337, + scheme: "http", + cred: build(:credential), + adapter: GRPC.Client.Adapters.Gun, + adapter_payload: %{}, + codec: GRPC.Codec.Proto, + interceptors: [], + compressor: nil, + accepted_compressors: [], + headers: [] + } + end + + def credential_factory do + cert_path = Path.expand("./tls/server1.pem", :code.priv_dir(:grpc)) + key_path = Path.expand("./tls/server1.key", :code.priv_dir(:grpc)) + ca_path = Path.expand("./tls/ca.pem", :code.priv_dir(:grpc)) + + %Credential{ + ssl: [ + certfile: cert_path, + cacertfile: ca_path, + keyfile: key_path, + verify: :verify_none, + versions: [:"tlsv1.2", :"tlsv1.3"] + ] + } + end + + def client_stream_factory do + %GRPC.Client.Stream{ + __interface__: %{ + receive_data: &GRPC.Client.Stream.receive_data/2, + send_request: &GRPC.Client.Stream.send_request/3 + }, + canceled: false, + channel: build(:channel, adapter: GRPC.Client.Adapters.Mint), + codec: GRPC.Codec.Proto, + compressor: nil, + grpc_type: :unary, + headers: %{}, + method_name: "SayHello", + path: "/helloworld.Greeter/SayHello", + payload: %{}, + request_mod: Helloworld.HelloRequest, + response_mod: Helloworld.HelloReply, + rpc: {"say_hello", {Helloworld.HelloRequest, false}, {Helloworld.HelloReply, false}}, + server_stream: false, + service_name: "helloworld.Greeter", + accepted_compressors: [GRPC.Compressor.Gzip] + } + end + + def hello_reply_rpc_factory do + %Helloworld.HelloReply{message: "Hello Luis"} + end + + def bye_reply_rpc_factory do + %Helloworld.HelloReply{message: "Bye Luis"} + end +end diff --git a/test/support/feature_server.ex b/test/support/feature_server.ex new file mode 100644 index 000000000..d37225fc7 --- /dev/null +++ b/test/support/feature_server.ex @@ -0,0 +1,11 @@ +defmodule FeatureServer do + use GRPC.Server, service: Routeguide.RouteGuide.Service + + def get_feature(point, _stream) do + if point.latitude != 0 do + %Routeguide.Feature{location: point, name: "#{point.latitude},#{point.longitude}"} + else + {:error, "server error"} + end + end +end diff --git a/test/support/google/api/annotations.proto b/test/support/google/api/annotations.proto new file mode 100644 index 000000000..efdab3db6 --- /dev/null +++ b/test/support/google/api/annotations.proto @@ -0,0 +1,31 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +import "google/api/http.proto"; +import "google/protobuf/descriptor.proto"; + +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "AnnotationsProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +extend google.protobuf.MethodOptions { + // See `HttpRule`. + HttpRule http = 72295728; +} diff --git a/test/support/google/api/http.proto b/test/support/google/api/http.proto new file mode 100644 index 000000000..113fa936a --- /dev/null +++ b/test/support/google/api/http.proto @@ -0,0 +1,375 @@ +// Copyright 2015 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package google.api; + +option cc_enable_arenas = true; +option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations"; +option java_multiple_files = true; +option java_outer_classname = "HttpProto"; +option java_package = "com.google.api"; +option objc_class_prefix = "GAPI"; + +// Defines the HTTP configuration for an API service. It contains a list of +// [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method +// to one or more HTTP REST API methods. +message Http { + // A list of HTTP configuration rules that apply to individual API methods. + // + // **NOTE:** All service configuration rules follow "last one wins" order. + repeated HttpRule rules = 1; + + // When set to true, URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // + // The default behavior is to not decode RFC 6570 reserved characters in multi + // segment matches. + bool fully_decode_reserved_expansion = 2; +} + +// # gRPC Transcoding +// +// gRPC Transcoding is a feature for mapping between a gRPC method and one or +// more HTTP REST endpoints. It allows developers to build a single API service +// that supports both gRPC APIs and REST APIs. Many systems, including [Google +// APIs](https://github.com/googleapis/googleapis), +// [Cloud Endpoints](https://cloud.google.com/endpoints), [gRPC +// Gateway](https://github.com/grpc-ecosystem/grpc-gateway), +// and [Envoy](https://github.com/envoyproxy/envoy) proxy support this feature +// and use it for large scale production services. +// +// `HttpRule` defines the schema of the gRPC/REST mapping. The mapping specifies +// how different portions of the gRPC request message are mapped to the URL +// path, URL query parameters, and HTTP request body. It also controls how the +// gRPC response message is mapped to the HTTP response body. `HttpRule` is +// typically specified as an `google.api.http` annotation on the gRPC method. +// +// Each mapping specifies a URL path template and an HTTP method. The path +// template may refer to one or more fields in the gRPC request message, as long +// as each field is a non-repeated field with a primitive (non-message) type. +// The path template controls how fields of the request message are mapped to +// the URL path. +// +// Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/{name=messages/*}" +// }; +// } +// } +// message GetMessageRequest { +// string name = 1; // Mapped to URL path. +// } +// message Message { +// string text = 1; // The resource content. +// } +// +// This enables an HTTP REST to gRPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(name: "messages/123456")` +// +// Any fields in the request message which are not bound by the path template +// automatically become HTTP query parameters if there is no HTTP request body. +// For example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get:"/v1/messages/{message_id}" +// }; +// } +// } +// message GetMessageRequest { +// message SubMessage { +// string subfield = 1; +// } +// string message_id = 1; // Mapped to URL path. +// int64 revision = 2; // Mapped to URL query parameter `revision`. +// SubMessage sub = 3; // Mapped to URL query parameter `sub.subfield`. +// } +// +// This enables a HTTP JSON to RPC mapping as below: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456?revision=2&sub.subfield=foo` | +// `GetMessage(message_id: "123456" revision: 2 sub: SubMessage(subfield: +// "foo"))` +// +// Note that fields which are mapped to URL query parameters must have a +// primitive type or a repeated primitive type or a non-repeated message type. +// In the case of a repeated type, the parameter can be repeated in the URL +// as `...?param=A¶m=B`. In the case of a message type, each field of the +// message is mapped to a separate parameter, such as +// `...?foo.a=A&foo.b=B&foo.c=C`. +// +// For HTTP methods that allow a request body, the `body` field +// specifies the mapping. Consider a REST update method on the +// message resource collection: +// +// service Messaging { +// rpc UpdateMessage(UpdateMessageRequest) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "message" +// }; +// } +// } +// message UpdateMessageRequest { +// string message_id = 1; // mapped to the URL +// Message message = 2; // mapped to the body +// } +// +// The following HTTP JSON to RPC mapping is enabled, where the +// representation of the JSON in the request body is determined by +// protos JSON encoding: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" message { text: "Hi!" })` +// +// The special name `*` can be used in the body mapping to define that +// every field not bound by the path template should be mapped to the +// request body. This enables the following alternative definition of +// the update method: +// +// service Messaging { +// rpc UpdateMessage(Message) returns (Message) { +// option (google.api.http) = { +// patch: "/v1/messages/{message_id}" +// body: "*" +// }; +// } +// } +// message Message { +// string message_id = 1; +// string text = 2; +// } +// +// +// The following HTTP JSON to RPC mapping is enabled: +// +// HTTP | gRPC +// -----|----- +// `PATCH /v1/messages/123456 { "text": "Hi!" }` | `UpdateMessage(message_id: +// "123456" text: "Hi!")` +// +// Note that when using `*` in the body mapping, it is not possible to +// have HTTP parameters, as all fields not bound by the path end in +// the body. This makes this option more rarely used in practice when +// defining REST APIs. The common usage of `*` is in custom methods +// which don't use the URL at all for transferring data. +// +// It is possible to define multiple HTTP methods for one RPC by using +// the `additional_bindings` option. Example: +// +// service Messaging { +// rpc GetMessage(GetMessageRequest) returns (Message) { +// option (google.api.http) = { +// get: "/v1/messages/{message_id}" +// additional_bindings { +// get: "/v1/users/{user_id}/messages/{message_id}" +// } +// }; +// } +// } +// message GetMessageRequest { +// string message_id = 1; +// string user_id = 2; +// } +// +// This enables the following two alternative HTTP JSON to RPC mappings: +// +// HTTP | gRPC +// -----|----- +// `GET /v1/messages/123456` | `GetMessage(message_id: "123456")` +// `GET /v1/users/me/messages/123456` | `GetMessage(user_id: "me" message_id: +// "123456")` +// +// ## Rules for HTTP mapping +// +// 1. Leaf request fields (recursive expansion nested messages in the request +// message) are classified into three categories: +// - Fields referred by the path template. They are passed via the URL path. +// - Fields referred by the [HttpRule.body][google.api.HttpRule.body]. They are passed via the HTTP +// request body. +// - All other fields are passed via the URL query parameters, and the +// parameter name is the field path in the request message. A repeated +// field can be represented as multiple query parameters under the same +// name. +// 2. If [HttpRule.body][google.api.HttpRule.body] is "*", there is no URL query parameter, all fields +// are passed via URL path and HTTP request body. +// 3. If [HttpRule.body][google.api.HttpRule.body] is omitted, there is no HTTP request body, all +// fields are passed via URL path and URL query parameters. +// +// ### Path template syntax +// +// Template = "/" Segments [ Verb ] ; +// Segments = Segment { "/" Segment } ; +// Segment = "*" | "**" | LITERAL | Variable ; +// Variable = "{" FieldPath [ "=" Segments ] "}" ; +// FieldPath = IDENT { "." IDENT } ; +// Verb = ":" LITERAL ; +// +// The syntax `*` matches a single URL path segment. The syntax `**` matches +// zero or more URL path segments, which must be the last part of the URL path +// except the `Verb`. +// +// The syntax `Variable` matches part of the URL path as specified by its +// template. A variable template must not contain other variables. If a variable +// matches a single path segment, its template may be omitted, e.g. `{var}` +// is equivalent to `{var=*}`. +// +// The syntax `LITERAL` matches literal text in the URL path. If the `LITERAL` +// contains any reserved character, such characters should be percent-encoded +// before the matching. +// +// If a variable contains exactly one path segment, such as `"{var}"` or +// `"{var=*}"`, when such a variable is expanded into a URL path on the client +// side, all characters except `[-_.~0-9a-zA-Z]` are percent-encoded. The +// server side does the reverse decoding. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{var}`. +// +// If a variable contains multiple path segments, such as `"{var=foo/*}"` +// or `"{var=**}"`, when such a variable is expanded into a URL path on the +// client side, all characters except `[-_.~/0-9a-zA-Z]` are percent-encoded. +// The server side does the reverse decoding, except "%2F" and "%2f" are left +// unchanged. Such variables show up in the +// [Discovery +// Document](https://developers.google.com/discovery/v1/reference/apis) as +// `{+var}`. +// +// ## Using gRPC API Service Configuration +// +// gRPC API Service Configuration (service config) is a configuration language +// for configuring a gRPC service to become a user-facing product. The +// service config is simply the YAML representation of the `google.api.Service` +// proto message. +// +// As an alternative to annotating your proto file, you can configure gRPC +// transcoding in your service config YAML files. You do this by specifying a +// `HttpRule` that maps the gRPC method to a REST endpoint, achieving the same +// effect as the proto annotation. This can be particularly useful if you +// have a proto that is reused in multiple services. Note that any transcoding +// specified in the service config will override any matching transcoding +// configuration in the proto. +// +// Example: +// +// http: +// rules: +// # Selects a gRPC method and applies HttpRule to it. +// - selector: example.v1.Messaging.GetMessage +// get: /v1/messages/{message_id}/{sub.subfield} +// +// ## Special notes +// +// When gRPC Transcoding is used to map a gRPC to JSON REST endpoints, the +// proto to JSON conversion must follow the [proto3 +// specification](https://developers.google.com/protocol-buffers/docs/proto3#json). +// +// While the single segment variable follows the semantics of +// [RFC 6570](https://tools.ietf.org/html/rfc6570) Section 3.2.2 Simple String +// Expansion, the multi segment variable **does not** follow RFC 6570 Section +// 3.2.3 Reserved Expansion. The reason is that the Reserved Expansion +// does not expand special characters like `?` and `#`, which would lead +// to invalid URLs. As the result, gRPC Transcoding uses a custom encoding +// for multi segment variables. +// +// The path variables **must not** refer to any repeated or mapped field, +// because client libraries are not capable of handling such variable expansion. +// +// The path variables **must not** capture the leading "/" character. The reason +// is that the most common use case "{var}" does not capture the leading "/" +// character. For consistency, all path variables must share the same behavior. +// +// Repeated message fields must not be mapped to URL query parameters, because +// no client library can support such complicated mapping. +// +// If an API needs to use a JSON array for request or response body, it can map +// the request or response body to a repeated field. However, some gRPC +// Transcoding implementations may not support this feature. +message HttpRule { + // Selects a method to which this rule applies. + // + // Refer to [selector][google.api.DocumentationRule.selector] for syntax details. + string selector = 1; + + // Determines the URL pattern is matched by this rules. This pattern can be + // used with any of the {get|put|post|delete|patch} methods. A custom method + // can be defined using the 'custom' field. + oneof pattern { + // Maps to HTTP GET. Used for listing and getting information about + // resources. + string get = 2; + + // Maps to HTTP PUT. Used for replacing a resource. + string put = 3; + + // Maps to HTTP POST. Used for creating a resource or performing an action. + string post = 4; + + // Maps to HTTP DELETE. Used for deleting a resource. + string delete = 5; + + // Maps to HTTP PATCH. Used for updating a resource. + string patch = 6; + + // The custom pattern is used for specifying an HTTP method that is not + // included in the `pattern` field, such as HEAD, or "*" to leave the + // HTTP method unspecified for this rule. The wild-card rule is useful + // for services that provide content to Web (HTML) clients. + CustomHttpPattern custom = 8; + } + + // The name of the request field whose value is mapped to the HTTP request + // body, or `*` for mapping all request fields not captured by the path + // pattern to the HTTP body, or omitted for not having any HTTP request body. + // + // NOTE: the referred field must be present at the top-level of the request + // message type. + string body = 7; + + // Optional. The name of the response field whose value is mapped to the HTTP + // response body. When omitted, the entire response message will be used + // as the HTTP response body. + // + // NOTE: The referred field must be present at the top-level of the response + // message type. + string response_body = 12; + + // Additional HTTP bindings for the selector. Nested bindings must + // not contain an `additional_bindings` field themselves (that is, + // the nesting may only be one level deep). + repeated HttpRule additional_bindings = 11; +} + +// A custom pattern is used for defining custom HTTP verb. +message CustomHttpPattern { + // The name of this custom HTTP verb. + string kind = 1; + + // The path matched by this custom verb. + string path = 2; +} diff --git a/test/support/google/protobuf/descriptor.proto b/test/support/google/protobuf/descriptor.proto new file mode 100644 index 000000000..9b11d24ac --- /dev/null +++ b/test/support/google/protobuf/descriptor.proto @@ -0,0 +1,1223 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// https://developers.google.com/protocol-buffers/ +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// Author: kenton@google.com (Kenton Varda) +// Based on original Protocol Buffers design by +// Sanjay Ghemawat, Jeff Dean, and others. +// +// The messages in this file describe the definitions found in .proto files. +// A valid .proto file can be translated directly to a FileDescriptorProto +// without any other information (e.g. without reading its imports). + +syntax = "proto2"; + +package google.protobuf; + +option go_package = "google.golang.org/protobuf/types/descriptorpb"; +option java_package = "com.google.protobuf"; +option java_outer_classname = "DescriptorProtos"; +option csharp_namespace = "Google.Protobuf.Reflection"; +option objc_class_prefix = "GPB"; +option cc_enable_arenas = true; + +// descriptor.proto must be optimized for speed because reflection-based +// algorithms don't work during bootstrapping. +option optimize_for = SPEED; + +// The protocol compiler can output a FileDescriptorSet containing the .proto +// files it parses. +message FileDescriptorSet { + repeated FileDescriptorProto file = 1; +} + +// The full set of known editions. +enum Edition { + // A placeholder for an unknown edition value. + EDITION_UNKNOWN = 0; + + // Legacy syntax "editions". These pre-date editions, but behave much like + // distinct editions. These can't be used to specify the edition of proto + // files, but feature definitions must supply proto2/proto3 defaults for + // backwards compatibility. + EDITION_PROTO2 = 998; + EDITION_PROTO3 = 999; + + // Editions that have been released. The specific values are arbitrary and + // should not be depended on, but they will always be time-ordered for easy + // comparison. + EDITION_2023 = 1000; + EDITION_2024 = 1001; + + // Placeholder editions for testing feature resolution. These should not be + // used or relyed on outside of tests. + EDITION_1_TEST_ONLY = 1; + EDITION_2_TEST_ONLY = 2; + EDITION_99997_TEST_ONLY = 99997; + EDITION_99998_TEST_ONLY = 99998; + EDITION_99999_TEST_ONLY = 99999; + + // Placeholder for specifying unbounded edition support. This should only + // ever be used by plugins that can expect to never require any changes to + // support a new edition. + EDITION_MAX = 0x7FFFFFFF; +} + +// Describes a complete .proto file. +message FileDescriptorProto { + optional string name = 1; // file name, relative to root of source tree + optional string package = 2; // e.g. "foo", "foo.bar", etc. + + // Names of files imported by this file. + repeated string dependency = 3; + // Indexes of the public imported files in the dependency list above. + repeated int32 public_dependency = 10; + // Indexes of the weak imported files in the dependency list. + // For Google-internal migration only. Do not use. + repeated int32 weak_dependency = 11; + + // All top-level definitions in this file. + repeated DescriptorProto message_type = 4; + repeated EnumDescriptorProto enum_type = 5; + repeated ServiceDescriptorProto service = 6; + repeated FieldDescriptorProto extension = 7; + + optional FileOptions options = 8; + + // This field contains optional information about the original source code. + // You may safely remove this entire field without harming runtime + // functionality of the descriptors -- the information is needed only by + // development tools. + optional SourceCodeInfo source_code_info = 9; + + // The syntax of the proto file. + // The supported values are "proto2", "proto3", and "editions". + // + // If `edition` is present, this value must be "editions". + optional string syntax = 12; + + // The edition of the proto file. + optional Edition edition = 14; +} + +// Describes a message type. +message DescriptorProto { + optional string name = 1; + + repeated FieldDescriptorProto field = 2; + repeated FieldDescriptorProto extension = 6; + + repeated DescriptorProto nested_type = 3; + repeated EnumDescriptorProto enum_type = 4; + + message ExtensionRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + + optional ExtensionRangeOptions options = 3; + } + repeated ExtensionRange extension_range = 5; + + repeated OneofDescriptorProto oneof_decl = 8; + + optional MessageOptions options = 7; + + // Range of reserved tag numbers. Reserved tag numbers may not be used by + // fields or extension ranges in the same message. Reserved ranges may + // not overlap. + message ReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Exclusive. + } + repeated ReservedRange reserved_range = 9; + // Reserved field names, which may not be used by fields in the same message. + // A given name may only be reserved once. + repeated string reserved_name = 10; +} + +message ExtensionRangeOptions { + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + message Declaration { + // The extension number declared within the extension range. + optional int32 number = 1; + + // The fully-qualified name of the extension field. There must be a leading + // dot in front of the full name. + optional string full_name = 2; + + // The fully-qualified type name of the extension field. Unlike + // Metadata.type, Declaration.type must have a leading dot for messages + // and enums. + optional string type = 3; + + // If true, indicates that the number is reserved in the extension range, + // and any extension field with the number will fail to compile. Set this + // when a declared extension field is deleted. + optional bool reserved = 5; + + // If true, indicates that the extension must be defined as repeated. + // Otherwise the extension must be defined as optional. + optional bool repeated = 6; + + reserved 4; // removed is_repeated + } + + // For external users: DO NOT USE. We are in the process of open sourcing + // extension declaration and executing internal cleanups before it can be + // used externally. + repeated Declaration declaration = 2 [retention = RETENTION_SOURCE]; + + // Any features defined in the specific edition. + optional FeatureSet features = 50; + + // The verification state of the extension range. + enum VerificationState { + // All the extensions of the range must be declared. + DECLARATION = 0; + UNVERIFIED = 1; + } + + // The verification state of the range. + // TODO: flip the default to DECLARATION once all empty ranges + // are marked as UNVERIFIED. + optional VerificationState verification = 3 + [default = UNVERIFIED, retention = RETENTION_SOURCE]; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// Describes a field within a message. +message FieldDescriptorProto { + enum Type { + // 0 is reserved for errors. + // Order is weird for historical reasons. + TYPE_DOUBLE = 1; + TYPE_FLOAT = 2; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT64 if + // negative values are likely. + TYPE_INT64 = 3; + TYPE_UINT64 = 4; + // Not ZigZag encoded. Negative numbers take 10 bytes. Use TYPE_SINT32 if + // negative values are likely. + TYPE_INT32 = 5; + TYPE_FIXED64 = 6; + TYPE_FIXED32 = 7; + TYPE_BOOL = 8; + TYPE_STRING = 9; + // Tag-delimited aggregate. + // Group type is deprecated and not supported after google.protobuf. However, Proto3 + // implementations should still be able to parse the group wire format and + // treat group fields as unknown fields. In Editions, the group wire format + // can be enabled via the `message_encoding` feature. + TYPE_GROUP = 10; + TYPE_MESSAGE = 11; // Length-delimited aggregate. + + // New in version 2. + TYPE_BYTES = 12; + TYPE_UINT32 = 13; + TYPE_ENUM = 14; + TYPE_SFIXED32 = 15; + TYPE_SFIXED64 = 16; + TYPE_SINT32 = 17; // Uses ZigZag encoding. + TYPE_SINT64 = 18; // Uses ZigZag encoding. + } + + enum Label { + // 0 is reserved for errors + LABEL_OPTIONAL = 1; + LABEL_REPEATED = 3; + // The required label is only allowed in google.protobuf. In proto3 and Editions + // it's explicitly prohibited. In Editions, the `field_presence` feature + // can be used to get this behavior. + LABEL_REQUIRED = 2; + } + + optional string name = 1; + optional int32 number = 3; + optional Label label = 4; + + // If type_name is set, this need not be set. If both this and type_name + // are set, this must be one of TYPE_ENUM, TYPE_MESSAGE or TYPE_GROUP. + optional Type type = 5; + + // For message and enum types, this is the name of the type. If the name + // starts with a '.', it is fully-qualified. Otherwise, C++-like scoping + // rules are used to find the type (i.e. first the nested types within this + // message are searched, then within the parent, on up to the root + // namespace). + optional string type_name = 6; + + // For extensions, this is the name of the type being extended. It is + // resolved in the same manner as type_name. + optional string extendee = 2; + + // For numeric types, contains the original text representation of the value. + // For booleans, "true" or "false". + // For strings, contains the default text contents (not escaped in any way). + // For bytes, contains the C escaped value. All bytes >= 128 are escaped. + optional string default_value = 7; + + // If set, gives the index of a oneof in the containing type's oneof_decl + // list. This field is a member of that oneof. + optional int32 oneof_index = 9; + + // JSON name of this field. The value is set by protocol compiler. If the + // user has set a "json_name" option on this field, that option's value + // will be used. Otherwise, it's deduced from the field's name by converting + // it to camelCase. + optional string json_name = 10; + + optional FieldOptions options = 8; + + // If true, this is a proto3 "optional". When a proto3 field is optional, it + // tracks presence regardless of field type. + // + // When proto3_optional is true, this field must belong to a oneof to signal + // to old proto3 clients that presence is tracked for this field. This oneof + // is known as a "synthetic" oneof, and this field must be its sole member + // (each proto3 optional field gets its own synthetic oneof). Synthetic oneofs + // exist in the descriptor only, and do not generate any API. Synthetic oneofs + // must be ordered after all "real" oneofs. + // + // For message fields, proto3_optional doesn't create any semantic change, + // since non-repeated message fields always track presence. However it still + // indicates the semantic detail of whether the user wrote "optional" or not. + // This can be useful for round-tripping the .proto file. For consistency we + // give message fields a synthetic oneof also, even though it is not required + // to track presence. This is especially important because the parser can't + // tell if a field is a message or an enum, so it must always create a + // synthetic oneof. + // + // Proto2 optional fields do not set this flag, because they already indicate + // optional with `LABEL_OPTIONAL`. + optional bool proto3_optional = 17; +} + +// Describes a oneof. +message OneofDescriptorProto { + optional string name = 1; + optional OneofOptions options = 2; +} + +// Describes an enum type. +message EnumDescriptorProto { + optional string name = 1; + + repeated EnumValueDescriptorProto value = 2; + + optional EnumOptions options = 3; + + // Range of reserved numeric values. Reserved values may not be used by + // entries in the same enum. Reserved ranges may not overlap. + // + // Note that this is distinct from DescriptorProto.ReservedRange in that it + // is inclusive such that it can appropriately represent the entire int32 + // domain. + message EnumReservedRange { + optional int32 start = 1; // Inclusive. + optional int32 end = 2; // Inclusive. + } + + // Range of reserved numeric values. Reserved numeric values may not be used + // by enum values in the same enum declaration. Reserved ranges may not + // overlap. + repeated EnumReservedRange reserved_range = 4; + + // Reserved enum value names, which may not be reused. A given name may only + // be reserved once. + repeated string reserved_name = 5; +} + +// Describes a value within an enum. +message EnumValueDescriptorProto { + optional string name = 1; + optional int32 number = 2; + + optional EnumValueOptions options = 3; +} + +// Describes a service. +message ServiceDescriptorProto { + optional string name = 1; + repeated MethodDescriptorProto method = 2; + + optional ServiceOptions options = 3; +} + +// Describes a method of a service. +message MethodDescriptorProto { + optional string name = 1; + + // Input and output type names. These are resolved in the same way as + // FieldDescriptorProto.type_name, but must refer to a message type. + optional string input_type = 2; + optional string output_type = 3; + + optional MethodOptions options = 4; + + // Identifies if client streams multiple client messages + optional bool client_streaming = 5 [default = false]; + // Identifies if server streams multiple server messages + optional bool server_streaming = 6 [default = false]; +} + +// =================================================================== +// Options + +// Each of the definitions above may have "options" attached. These are +// just annotations which may cause code to be generated slightly differently +// or may contain hints for code that manipulates protocol messages. +// +// Clients may define custom options as extensions of the *Options messages. +// These extensions may not yet be known at parsing time, so the parser cannot +// store the values in them. Instead it stores them in a field in the *Options +// message called uninterpreted_option. This field must have the same name +// across all *Options messages. We then use this field to populate the +// extensions when we build a descriptor, at which point all protos have been +// parsed and so all extensions are known. +// +// Extension numbers for custom options may be chosen as follows: +// * For options which will only be used within a single application or +// organization, or for experimental options, use field numbers 50000 +// through 99999. It is up to you to ensure that you do not use the +// same number for multiple options. +// * For options which will be published and used publicly by multiple +// independent entities, e-mail protobuf-global-extension-registry@google.com +// to reserve extension numbers. Simply provide your project name (e.g. +// Objective-C plugin) and your project website (if available) -- there's no +// need to explain how you intend to use them. Usually you only need one +// extension number. You can declare multiple options with only one extension +// number by putting them in a sub-message. See the Custom Options section of +// the docs for examples: +// https://developers.google.com/protocol-buffers/docs/proto#options +// If this turns out to be popular, a web service will be set up +// to automatically assign option numbers. + +message FileOptions { + + // Sets the Java package where classes generated from this .proto will be + // placed. By default, the proto package is used, but this is often + // inappropriate because proto packages do not normally start with backwards + // domain names. + optional string java_package = 1; + + // Controls the name of the wrapper Java class generated for the .proto file. + // That class will always contain the .proto file's getDescriptor() method as + // well as any top-level extensions defined in the .proto file. + // If java_multiple_files is disabled, then all the other classes from the + // .proto file will be nested inside the single wrapper outer class. + optional string java_outer_classname = 8; + + // If enabled, then the Java code generator will generate a separate .java + // file for each top-level message, enum, and service defined in the .proto + // file. Thus, these types will *not* be nested inside the wrapper class + // named by java_outer_classname. However, the wrapper class will still be + // generated to contain the file's getDescriptor() method as well as any + // top-level extensions defined in the file. + optional bool java_multiple_files = 10 [default = false]; + + // This option does nothing. + optional bool java_generate_equals_and_hash = 20 [deprecated=true]; + + // A proto2 file can set this to true to opt in to UTF-8 checking for Java, + // which will throw an exception if invalid UTF-8 is parsed from the wire or + // assigned to a string field. + // + // TODO: clarify exactly what kinds of field types this option + // applies to, and update these docs accordingly. + // + // Proto3 files already perform these checks. Setting the option explicitly to + // false has no effect: it cannot be used to opt proto3 files out of UTF-8 + // checks. + optional bool java_string_check_utf8 = 27 [default = false]; + + // Generated classes can be optimized for speed or code size. + enum OptimizeMode { + SPEED = 1; // Generate complete code for parsing, serialization, + // etc. + CODE_SIZE = 2; // Use ReflectionOps to implement these methods. + LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime. + } + optional OptimizeMode optimize_for = 9 [default = SPEED]; + + // Sets the Go package where structs generated from this .proto will be + // placed. If omitted, the Go package will be derived from the following: + // - The basename of the package import path, if provided. + // - Otherwise, the package statement in the .proto file, if present. + // - Otherwise, the basename of the .proto file, without extension. + optional string go_package = 11; + + // Should generic services be generated in each language? "Generic" services + // are not specific to any particular RPC system. They are generated by the + // main code generators in each language (without additional plugins). + // Generic services were the only kind of service generation supported by + // early versions of google.protobuf. + // + // Generic services are now considered deprecated in favor of using plugins + // that generate code specific to your particular RPC system. Therefore, + // these default to false. Old code which depends on generic services should + // explicitly set them to true. + optional bool cc_generic_services = 16 [default = false]; + optional bool java_generic_services = 17 [default = false]; + optional bool py_generic_services = 18 [default = false]; + reserved 42; // removed php_generic_services + + // Is this file deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for everything in the file, or it will be completely ignored; in the very + // least, this is a formalization for deprecating files. + optional bool deprecated = 23 [default = false]; + + // Enables the use of arenas for the proto messages in this file. This applies + // only to generated classes for C++. + optional bool cc_enable_arenas = 31 [default = true]; + + // Sets the objective c class prefix which is prepended to all objective c + // generated classes from this .proto. There is no default. + optional string objc_class_prefix = 36; + + // Namespace for generated classes; defaults to the package. + optional string csharp_namespace = 37; + + // By default Swift generators will take the proto package and CamelCase it + // replacing '.' with underscore and use that to prefix the types/symbols + // defined. When this options is provided, they will use this value instead + // to prefix the types/symbols defined. + optional string swift_prefix = 39; + + // Sets the php class prefix which is prepended to all php generated classes + // from this .proto. Default is empty. + optional string php_class_prefix = 40; + + // Use this option to change the namespace of php generated classes. Default + // is empty. When this option is empty, the package name will be used for + // determining the namespace. + optional string php_namespace = 41; + + // Use this option to change the namespace of php generated metadata classes. + // Default is empty. When this option is empty, the proto file name will be + // used for determining the namespace. + optional string php_metadata_namespace = 44; + + // Use this option to change the package of ruby generated classes. Default + // is empty. When this option is not set, the package name will be used for + // determining the ruby package. + optional string ruby_package = 45; + + // Any features defined in the specific edition. + optional FeatureSet features = 50; + + // The parser stores options it doesn't recognize here. + // See the documentation for the "Options" section above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. + // See the documentation for the "Options" section above. + extensions 1000 to max; + + reserved 38; +} + +message MessageOptions { + // Set true to use the old proto1 MessageSet wire format for extensions. + // This is provided for backwards-compatibility with the MessageSet wire + // format. You should not use this for any other reason: It's less + // efficient, has fewer features, and is more complicated. + // + // The message must be defined exactly as follows: + // message Foo { + // option message_set_wire_format = true; + // extensions 4 to max; + // } + // Note that the message cannot have any defined fields; MessageSets only + // have extensions. + // + // All extensions of your type must be singular messages; e.g. they cannot + // be int32s, enums, or repeated messages. + // + // Because this is an option, the above two restrictions are not enforced by + // the protocol compiler. + optional bool message_set_wire_format = 1 [default = false]; + + // Disables the generation of the standard "descriptor()" accessor, which can + // conflict with a field of the same name. This is meant to make migration + // from proto1 easier; new code should avoid fields named "descriptor". + optional bool no_standard_descriptor_accessor = 2 [default = false]; + + // Is this message deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the message, or it will be completely ignored; in the very least, + // this is a formalization for deprecating messages. + optional bool deprecated = 3 [default = false]; + + reserved 4, 5, 6; + + // Whether the message is an automatically generated map entry type for the + // maps field. + // + // For maps fields: + // map map_field = 1; + // The parsed descriptor looks like: + // message MapFieldEntry { + // option map_entry = true; + // optional KeyType key = 1; + // optional ValueType value = 2; + // } + // repeated MapFieldEntry map_field = 1; + // + // Implementations may choose not to generate the map_entry=true message, but + // use a native map in the target language to hold the keys and values. + // The reflection APIs in such implementations still need to work as + // if the field is a repeated message field. + // + // NOTE: Do not set the option in .proto files. Always use the maps syntax + // instead. The option should only be implicitly set by the proto compiler + // parser. + optional bool map_entry = 7; + + reserved 8; // javalite_serializable + reserved 9; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // + // This should only be used as a temporary measure against broken builds due + // to the change in behavior for JSON field name conflicts. + // + // TODO This is legacy behavior we plan to remove once downstream + // teams have had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 11 [deprecated = true]; + + // Any features defined in the specific edition. + optional FeatureSet features = 12; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message FieldOptions { + // The ctype option instructs the C++ code generator to use a different + // representation of the field than it normally would. See the specific + // options below. This option is only implemented to support use of + // [ctype=CORD] and [ctype=STRING] (the default) on non-repeated fields of + // type "bytes" in the open source release -- sorry, we'll try to include + // other types in a future version! + optional CType ctype = 1 [default = STRING]; + enum CType { + // Default mode. + STRING = 0; + + // The option [ctype=CORD] may be applied to a non-repeated field of type + // "bytes". It indicates that in C++, the data should be stored in a Cord + // instead of a string. For very large strings, this may reduce memory + // fragmentation. It may also allow better performance when parsing from a + // Cord, or when parsing with aliasing enabled, as the parsed Cord may then + // alias the original buffer. + CORD = 1; + + STRING_PIECE = 2; + } + // The packed option can be enabled for repeated primitive fields to enable + // a more efficient representation on the wire. Rather than repeatedly + // writing the tag and type for each element, the entire array is encoded as + // a single length-delimited blob. In proto3, only explicit setting it to + // false will avoid using packed encoding. This option is prohibited in + // Editions, but the `repeated_field_encoding` feature can be used to control + // the behavior. + optional bool packed = 2; + + // The jstype option determines the JavaScript type used for values of the + // field. The option is permitted only for 64 bit integral and fixed types + // (int64, uint64, sint64, fixed64, sfixed64). A field with jstype JS_STRING + // is represented as JavaScript string, which avoids loss of precision that + // can happen when a large value is converted to a floating point JavaScript. + // Specifying JS_NUMBER for the jstype causes the generated JavaScript code to + // use the JavaScript "number" type. The behavior of the default option + // JS_NORMAL is implementation dependent. + // + // This option is an enum to permit additional types to be added, e.g. + // goog.math.Integer. + optional JSType jstype = 6 [default = JS_NORMAL]; + enum JSType { + // Use the default type. + JS_NORMAL = 0; + + // Use JavaScript strings. + JS_STRING = 1; + + // Use JavaScript numbers. + JS_NUMBER = 2; + } + + // Should this field be parsed lazily? Lazy applies only to message-type + // fields. It means that when the outer message is initially parsed, the + // inner message's contents will not be parsed but instead stored in encoded + // form. The inner message will actually be parsed when it is first accessed. + // + // This is only a hint. Implementations are free to choose whether to use + // eager or lazy parsing regardless of the value of this option. However, + // setting this option true suggests that the protocol author believes that + // using lazy parsing on this field is worth the additional bookkeeping + // overhead typically needed to implement it. + // + // This option does not affect the public interface of any generated code; + // all method signatures remain the same. Furthermore, thread-safety of the + // interface is not affected by this option; const methods remain safe to + // call from multiple threads concurrently, while non-const methods continue + // to require exclusive access. + // + // Note that lazy message fields are still eagerly verified to check + // ill-formed wireformat or missing required fields. Calling IsInitialized() + // on the outer message would fail if the inner message has missing required + // fields. Failed verification would result in parsing failure (except when + // uninitialized messages are acceptable). + optional bool lazy = 5 [default = false]; + + // unverified_lazy does no correctness checks on the byte stream. This should + // only be used where lazy with verification is prohibitive for performance + // reasons. + optional bool unverified_lazy = 15 [default = false]; + + // Is this field deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for accessors, or it will be completely ignored; in the very least, this + // is a formalization for deprecating fields. + optional bool deprecated = 3 [default = false]; + + // For Google-internal migration only. Do not use. + optional bool weak = 10 [default = false]; + + // Indicate that the field value should not be printed out when using debug + // formats, e.g. when the field contains sensitive credentials. + optional bool debug_redact = 16 [default = false]; + + // If set to RETENTION_SOURCE, the option will be omitted from the binary. + // Note: as of January 2023, support for this is in progress and does not yet + // have an effect (b/264593489). + enum OptionRetention { + RETENTION_UNKNOWN = 0; + RETENTION_RUNTIME = 1; + RETENTION_SOURCE = 2; + } + + optional OptionRetention retention = 17; + + // This indicates the types of entities that the field may apply to when used + // as an option. If it is unset, then the field may be freely used as an + // option on any kind of entity. Note: as of January 2023, support for this is + // in progress and does not yet have an effect (b/264593489). + enum OptionTargetType { + TARGET_TYPE_UNKNOWN = 0; + TARGET_TYPE_FILE = 1; + TARGET_TYPE_EXTENSION_RANGE = 2; + TARGET_TYPE_MESSAGE = 3; + TARGET_TYPE_FIELD = 4; + TARGET_TYPE_ONEOF = 5; + TARGET_TYPE_ENUM = 6; + TARGET_TYPE_ENUM_ENTRY = 7; + TARGET_TYPE_SERVICE = 8; + TARGET_TYPE_METHOD = 9; + } + + repeated OptionTargetType targets = 19; + + message EditionDefault { + optional Edition edition = 3; + optional string value = 2; // Textproto value. + } + repeated EditionDefault edition_defaults = 20; + + // Any features defined in the specific edition. + optional FeatureSet features = 21; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; + + reserved 4; // removed jtype + reserved 18; // reserve target, target_obsolete_do_not_use +} + +message OneofOptions { + // Any features defined in the specific edition. + optional FeatureSet features = 1; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumOptions { + + // Set this option to true to allow mapping different tag names to the same + // value. + optional bool allow_alias = 2; + + // Is this enum deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum, or it will be completely ignored; in the very least, this + // is a formalization for deprecating enums. + optional bool deprecated = 3 [default = false]; + + reserved 5; // javanano_as_lite + + // Enable the legacy handling of JSON field name conflicts. This lowercases + // and strips underscored from the fields before comparison in proto3 only. + // The new behavior takes `json_name` into account and applies to proto2 as + // well. + // TODO Remove this legacy behavior once downstream teams have + // had time to migrate. + optional bool deprecated_legacy_json_field_conflicts = 6 [deprecated = true]; + + // Any features defined in the specific edition. + optional FeatureSet features = 7; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message EnumValueOptions { + // Is this enum value deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the enum value, or it will be completely ignored; in the very least, + // this is a formalization for deprecating enum values. + optional bool deprecated = 1 [default = false]; + + // Any features defined in the specific edition. + optional FeatureSet features = 2; + + // Indicate that fields annotated with this enum value should not be printed + // out when using debug formats, e.g. when the field contains sensitive + // credentials. + optional bool debug_redact = 3 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message ServiceOptions { + + // Any features defined in the specific edition. + optional FeatureSet features = 34; + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this service deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the service, or it will be completely ignored; in the very least, + // this is a formalization for deprecating services. + optional bool deprecated = 33 [default = false]; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +message MethodOptions { + + // Note: Field numbers 1 through 32 are reserved for Google's internal RPC + // framework. We apologize for hoarding these numbers to ourselves, but + // we were already using them long before we decided to release Protocol + // Buffers. + + // Is this method deprecated? + // Depending on the target platform, this can emit Deprecated annotations + // for the method, or it will be completely ignored; in the very least, + // this is a formalization for deprecating methods. + optional bool deprecated = 33 [default = false]; + + // Is this method side-effect-free (or safe in HTTP parlance), or idempotent, + // or neither? HTTP based RPC implementation may choose GET verb for safe + // methods, and PUT verb for idempotent methods instead of the default POST. + enum IdempotencyLevel { + IDEMPOTENCY_UNKNOWN = 0; + NO_SIDE_EFFECTS = 1; // implies idempotent + IDEMPOTENT = 2; // idempotent, but may have side effects + } + optional IdempotencyLevel idempotency_level = 34 + [default = IDEMPOTENCY_UNKNOWN]; + + // Any features defined in the specific edition. + optional FeatureSet features = 35; + + // The parser stores options it doesn't recognize here. See above. + repeated UninterpretedOption uninterpreted_option = 999; + + // Clients can define custom options in extensions of this message. See above. + extensions 1000 to max; +} + +// A message representing a option the parser does not recognize. This only +// appears in options protos created by the compiler::Parser class. +// DescriptorPool resolves these when building Descriptor objects. Therefore, +// options protos in descriptor objects (e.g. returned by Descriptor::options(), +// or produced by Descriptor::CopyTo()) will never have UninterpretedOptions +// in them. +message UninterpretedOption { + // The name of the uninterpreted option. Each string represents a segment in + // a dot-separated name. is_extension is true iff a segment represents an + // extension (denoted with parentheses in options specs in .proto files). + // E.g.,{ ["foo", false], ["bar.baz", true], ["moo", false] } represents + // "foo.(bar.baz).moo". + message NamePart { + required string name_part = 1; + required bool is_extension = 2; + } + repeated NamePart name = 2; + + // The value of the uninterpreted option, in whatever type the tokenizer + // identified it as during parsing. Exactly one of these should be set. + optional string identifier_value = 3; + optional uint64 positive_int_value = 4; + optional int64 negative_int_value = 5; + optional double double_value = 6; + optional bytes string_value = 7; + optional string aggregate_value = 8; +} + +// =================================================================== +// Features + +// TODO Enums in C++ gencode (and potentially other languages) are +// not well scoped. This means that each of the feature enums below can clash +// with each other. The short names we've chosen maximize call-site +// readability, but leave us very open to this scenario. A future feature will +// be designed and implemented to handle this, hopefully before we ever hit a +// conflict here. +message FeatureSet { + enum FieldPresence { + FIELD_PRESENCE_UNKNOWN = 0; + EXPLICIT = 1; + IMPLICIT = 2; + LEGACY_REQUIRED = 3; + } + optional FieldPresence field_presence = 1 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "EXPLICIT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "IMPLICIT" }, + edition_defaults = { edition: EDITION_2023, value: "EXPLICIT" } + ]; + + enum EnumType { + ENUM_TYPE_UNKNOWN = 0; + OPEN = 1; + CLOSED = 2; + } + optional EnumType enum_type = 2 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "CLOSED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "OPEN" } + ]; + + enum RepeatedFieldEncoding { + REPEATED_FIELD_ENCODING_UNKNOWN = 0; + PACKED = 1; + EXPANDED = 2; + } + optional RepeatedFieldEncoding repeated_field_encoding = 3 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "EXPANDED" }, + edition_defaults = { edition: EDITION_PROTO3, value: "PACKED" } + ]; + + enum Utf8Validation { + UTF8_VALIDATION_UNKNOWN = 0; + VERIFY = 2; + NONE = 3; + } + optional Utf8Validation utf8_validation = 4 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "NONE" }, + edition_defaults = { edition: EDITION_PROTO3, value: "VERIFY" } + ]; + + enum MessageEncoding { + MESSAGE_ENCODING_UNKNOWN = 0; + LENGTH_PREFIXED = 1; + DELIMITED = 2; + } + optional MessageEncoding message_encoding = 5 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_FIELD, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "LENGTH_PREFIXED" } + ]; + + enum JsonFormat { + JSON_FORMAT_UNKNOWN = 0; + ALLOW = 1; + LEGACY_BEST_EFFORT = 2; + } + optional JsonFormat json_format = 6 [ + retention = RETENTION_RUNTIME, + targets = TARGET_TYPE_MESSAGE, + targets = TARGET_TYPE_ENUM, + targets = TARGET_TYPE_FILE, + edition_defaults = { edition: EDITION_PROTO2, value: "LEGACY_BEST_EFFORT" }, + edition_defaults = { edition: EDITION_PROTO3, value: "ALLOW" } + ]; + + reserved 999; + + extensions 1000; // for Protobuf C++ + extensions 1001; // for Protobuf Java + extensions 1002; // for Protobuf Go + + extensions 9995 to 9999; // For internal testing + extensions 10000; // for https://github.com/bufbuild/protobuf-es +} + +// A compiled specification for the defaults of a set of features. These +// messages are generated from FeatureSet extensions and can be used to seed +// feature resolution. The resolution with this object becomes a simple search +// for the closest matching edition, followed by proto merges. +message FeatureSetDefaults { + // A map from every known edition with a unique set of defaults to its + // defaults. Not all editions may be contained here. For a given edition, + // the defaults at the closest matching edition ordered at or before it should + // be used. This field must be in strict ascending order by edition. + message FeatureSetEditionDefault { + optional Edition edition = 3; + optional FeatureSet features = 2; + } + repeated FeatureSetEditionDefault defaults = 1; + + // The minimum supported edition (inclusive) when this was constructed. + // Editions before this will not have defaults. + optional Edition minimum_edition = 4; + + // The maximum known edition (inclusive) when this was constructed. Editions + // after this will not have reliable defaults. + optional Edition maximum_edition = 5; +} + +// =================================================================== +// Optional source code info + +// Encapsulates information about the original source file from which a +// FileDescriptorProto was generated. +message SourceCodeInfo { + // A Location identifies a piece of source code in a .proto file which + // corresponds to a particular definition. This information is intended + // to be useful to IDEs, code indexers, documentation generators, and similar + // tools. + // + // For example, say we have a file like: + // message Foo { + // optional string foo = 1; + // } + // Let's look at just the field definition: + // optional string foo = 1; + // ^ ^^ ^^ ^ ^^^ + // a bc de f ghi + // We have the following locations: + // span path represents + // [a,i) [ 4, 0, 2, 0 ] The whole field definition. + // [a,b) [ 4, 0, 2, 0, 4 ] The label (optional). + // [c,d) [ 4, 0, 2, 0, 5 ] The type (string). + // [e,f) [ 4, 0, 2, 0, 1 ] The name (foo). + // [g,h) [ 4, 0, 2, 0, 3 ] The number (1). + // + // Notes: + // - A location may refer to a repeated field itself (i.e. not to any + // particular index within it). This is used whenever a set of elements are + // logically enclosed in a single code segment. For example, an entire + // extend block (possibly containing multiple extension definitions) will + // have an outer location whose path refers to the "extensions" repeated + // field without an index. + // - Multiple locations may have the same path. This happens when a single + // logical declaration is spread out across multiple places. The most + // obvious example is the "extend" block again -- there may be multiple + // extend blocks in the same scope, each of which will have the same path. + // - A location's span is not always a subset of its parent's span. For + // example, the "extendee" of an extension declaration appears at the + // beginning of the "extend" block and is shared by all extensions within + // the block. + // - Just because a location's span is a subset of some other location's span + // does not mean that it is a descendant. For example, a "group" defines + // both a type and a field in a single declaration. Thus, the locations + // corresponding to the type and field and their components will overlap. + // - Code which tries to interpret locations should probably be designed to + // ignore those that it doesn't understand, as more types of locations could + // be recorded in the future. + repeated Location location = 1; + message Location { + // Identifies which part of the FileDescriptorProto was defined at this + // location. + // + // Each element is a field number or an index. They form a path from + // the root FileDescriptorProto to the place where the definition appears. + // For example, this path: + // [ 4, 3, 2, 7, 1 ] + // refers to: + // file.message_type(3) // 4, 3 + // .field(7) // 2, 7 + // .name() // 1 + // This is because FileDescriptorProto.message_type has field number 4: + // repeated DescriptorProto message_type = 4; + // and DescriptorProto.field has field number 2: + // repeated FieldDescriptorProto field = 2; + // and FieldDescriptorProto.name has field number 1: + // optional string name = 1; + // + // Thus, the above path gives the location of a field name. If we removed + // the last element: + // [ 4, 3, 2, 7 ] + // this path refers to the whole field declaration (from the beginning + // of the label to the terminating semicolon). + repeated int32 path = 1 [packed = true]; + + // Always has exactly three or four elements: start line, start column, + // end line (optional, otherwise assumed same as start line), end column. + // These are packed into a single field for efficiency. Note that line + // and column numbers are zero-based -- typically you will want to add + // 1 to each before displaying to a user. + repeated int32 span = 2 [packed = true]; + + // If this SourceCodeInfo represents a complete declaration, these are any + // comments appearing before and after the declaration which appear to be + // attached to the declaration. + // + // A series of line comments appearing on consecutive lines, with no other + // tokens appearing on those lines, will be treated as a single comment. + // + // leading_detached_comments will keep paragraphs of comments that appear + // before (but not connected to) the current element. Each paragraph, + // separated by empty lines, will be one comment element in the repeated + // field. + // + // Only the comment content is provided; comment markers (e.g. //) are + // stripped out. For block comments, leading whitespace and an asterisk + // will be stripped from the beginning of each line other than the first. + // Newlines are included in the output. + // + // Examples: + // + // optional int32 foo = 1; // Comment attached to foo. + // // Comment attached to bar. + // optional int32 bar = 2; + // + // optional string baz = 3; + // // Comment attached to baz. + // // Another line attached to baz. + // + // // Comment attached to moo. + // // + // // Another line attached to moo. + // optional double moo = 4; + // + // // Detached comment for corge. This is not leading or trailing comments + // // to moo or corge because there are blank lines separating it from + // // both. + // + // // Detached comment for corge paragraph 2. + // + // optional string corge = 5; + // /* Block comment attached + // * to corge. Leading asterisks + // * will be removed. */ + // /* Block comment attached to + // * grault. */ + // optional int32 grault = 6; + // + // // ignored detached comments. + optional string leading_comments = 3; + optional string trailing_comments = 4; + repeated string leading_detached_comments = 6; + } +} + +// Describes the relationship between generated code and its original source +// file. A GeneratedCodeInfo message is associated with only one generated +// source file, but may contain references to different source .proto files. +message GeneratedCodeInfo { + // An Annotation connects some span of text in generated code to an element + // of its generating .proto file. + repeated Annotation annotation = 1; + message Annotation { + // Identifies the element in the original source .proto file. This field + // is formatted the same as SourceCodeInfo.Location.path. + repeated int32 path = 1 [packed = true]; + + // Identifies the filesystem path to the original source .proto. + optional string source_file = 2; + + // Identifies the starting offset in bytes in the generated code + // that relates to the identified object. + optional int32 begin = 3; + + // Identifies the ending offset in bytes in the generated code that + // relates to the identified object. The end offset should be one past + // the last relevant byte (so the length of the text = end - begin). + optional int32 end = 4; + + // Represents the identified object's effect on the element in the original + // .proto file. + enum Semantic { + // There is no effect or the effect is indescribable. + NONE = 0; + // The element is set or otherwise mutated. + SET = 1; + // An alias to the element is returned. + ALIAS = 2; + } + optional Semantic semantic = 5; + } +} \ No newline at end of file diff --git a/test/support/helloworld.pb.ex b/test/support/helloworld.pb.ex deleted file mode 100644 index f8758c318..000000000 --- a/test/support/helloworld.pb.ex +++ /dev/null @@ -1,56 +0,0 @@ -defmodule Helloworld.HelloRequest do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - name: String.t() - } - defstruct [:name] - - field :name, 1, type: :string -end - -defmodule Helloworld.HelloReply do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - message: String.t() - } - defstruct [:message] - - field :message, 1, type: :string -end - -defmodule Helloworld.HeaderRequest do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{} - defstruct [] -end - -defmodule Helloworld.HeaderReply do - @moduledoc false - use Protobuf, syntax: :proto3 - - @type t :: %__MODULE__{ - authorization: String.t() - } - defstruct [:authorization] - - field :authorization, 1, type: :string -end - -defmodule Helloworld.Greeter.Service do - @moduledoc false - use GRPC.Service, name: "helloworld.Greeter" - - rpc :SayHello, Helloworld.HelloRequest, Helloworld.HelloReply - rpc :CheckHeaders, Helloworld.HeaderRequest, Helloworld.HeaderReply -end - -defmodule Helloworld.Greeter.Stub do - @moduledoc false - use GRPC.Stub, service: Helloworld.Greeter.Service -end diff --git a/test/support/integration_test_case.ex b/test/support/integration_test_case.ex index df1daaa39..4a4dbe9a8 100644 --- a/test/support/integration_test_case.ex +++ b/test/support/integration_test_case.ex @@ -9,8 +9,8 @@ defmodule GRPC.Integration.TestCase do end end - def run_server(servers, func, port \\ 0) do - {:ok, _pid, port} = GRPC.Server.start(servers, port) + def run_server(servers, func, port \\ 0, opts \\ []) do + {:ok, _pid, port} = GRPC.Server.start(servers, port, opts) try do func.(port) @@ -37,7 +37,7 @@ defmodule GRPC.Integration.TestCase do result {:error, :eaddrinuse} -> - Logger.warn("Got eaddrinuse when reconnecting to #{server}:#{port}. retry: #{retry}") + Logger.warning("Got eaddrinuse when reconnecting to #{server}:#{port}. retry: #{retry}") if retry >= 1 do Process.sleep(500) @@ -50,4 +50,23 @@ defmodule GRPC.Integration.TestCase do result end end + + def attach_events(event_names) do + test_pid = self() + + handler_id = "handler-#{inspect(test_pid)}" + + :telemetry.attach_many( + handler_id, + event_names, + fn name, measurements, metadata, [] -> + send(test_pid, {name, measurements, metadata}) + end, + [] + ) + + on_exit(fn -> + :telemetry.detach(handler_id) + end) + end end diff --git a/test/support/proto/helloworld.pb.ex b/test/support/proto/helloworld.pb.ex new file mode 100644 index 000000000..4d9822528 --- /dev/null +++ b/test/support/proto/helloworld.pb.ex @@ -0,0 +1,42 @@ +defmodule Helloworld.HelloRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string + field :duration, 2, proto3_optional: true, type: :int32 +end + +defmodule Helloworld.HelloReply do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :message, 1, type: :string +end + +defmodule Helloworld.HeaderRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 +end + +defmodule Helloworld.HeaderReply do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :authorization, 1, type: :string +end + +defmodule Helloworld.Greeter.Service do + @moduledoc false + + use GRPC.Service, name: "helloworld.Greeter", protoc_gen_elixir_version: "0.14.1" + + rpc(:SayHello, Helloworld.HelloRequest, Helloworld.HelloReply, %{}) + + rpc(:CheckHeaders, Helloworld.HeaderRequest, Helloworld.HeaderReply, %{}) +end + +defmodule Helloworld.Greeter.Stub do + @moduledoc false + + use GRPC.Stub, service: Helloworld.Greeter.Service +end diff --git a/test/support/helloworld.proto b/test/support/proto/helloworld.proto similarity index 94% rename from test/support/helloworld.proto rename to test/support/proto/helloworld.proto index 0436df688..e0fac9669 100644 --- a/test/support/helloworld.proto +++ b/test/support/proto/helloworld.proto @@ -12,6 +12,7 @@ service Greeter { // The request message containing the user's name. message HelloRequest { string name = 1; + optional int32 duration = 2; } // The response message containing the greetings diff --git a/test/support/proto/route_guide.pb.ex b/test/support/proto/route_guide.pb.ex new file mode 100644 index 000000000..fdea75216 --- /dev/null +++ b/test/support/proto/route_guide.pb.ex @@ -0,0 +1,61 @@ +defmodule Routeguide.Point do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :latitude, 1, type: :int32 + field :longitude, 2, type: :int32 +end + +defmodule Routeguide.Rectangle do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :lo, 1, type: Routeguide.Point + field :hi, 2, type: Routeguide.Point +end + +defmodule Routeguide.Feature do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string + field :location, 2, type: Routeguide.Point +end + +defmodule Routeguide.RouteNote do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :location, 1, type: Routeguide.Point + field :message, 2, type: :string +end + +defmodule Routeguide.RouteSummary do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :point_count, 1, type: :int32, json_name: "pointCount" + field :feature_count, 2, type: :int32, json_name: "featureCount" + field :distance, 3, type: :int32 + field :elapsed_time, 4, type: :int32, json_name: "elapsedTime" +end + +defmodule Routeguide.RouteGuide.Service do + @moduledoc false + + use GRPC.Service, name: "routeguide.RouteGuide", protoc_gen_elixir_version: "0.14.1" + + rpc(:GetFeature, Routeguide.Point, Routeguide.Feature, %{}) + + rpc(:ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature), %{}) + + rpc(:RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary, %{}) + + rpc(:RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote), %{}) +end + +defmodule Routeguide.RouteGuide.Stub do + @moduledoc false + + use GRPC.Stub, service: Routeguide.RouteGuide.Service +end diff --git a/test/support/route_guide.proto b/test/support/proto/route_guide.proto similarity index 100% rename from test/support/route_guide.proto rename to test/support/proto/route_guide.proto diff --git a/test/support/route_guide.pb.ex b/test/support/route_guide.pb.ex deleted file mode 100644 index 6ad6f5592..000000000 --- a/test/support/route_guide.pb.ex +++ /dev/null @@ -1,82 +0,0 @@ -defmodule Routeguide.Point do - use Protobuf - - @type t :: %__MODULE__{ - latitude: integer, - longitude: integer - } - defstruct [:latitude, :longitude] - - field :latitude, 1, optional: true, type: :int32 - field :longitude, 2, optional: true, type: :int32 -end - -defmodule Routeguide.Rectangle do - use Protobuf - - @type t :: %__MODULE__{ - lo: Routeguide.Point.t(), - hi: Routeguide.Point.t() - } - defstruct [:lo, :hi] - - field :lo, 1, optional: true, type: Routeguide.Point - field :hi, 2, optional: true, type: Routeguide.Point -end - -defmodule Routeguide.Feature do - use Protobuf - - @type t :: %__MODULE__{ - name: String.t(), - location: Routeguide.Point.t() - } - defstruct [:name, :location] - - field :name, 1, optional: true, type: :string - field :location, 2, optional: true, type: Routeguide.Point -end - -defmodule Routeguide.RouteNote do - use Protobuf - - @type t :: %__MODULE__{ - location: Routeguide.Point.t(), - message: String.t() - } - defstruct [:location, :message] - - field :location, 1, optional: true, type: Routeguide.Point - field :message, 2, optional: true, type: :string -end - -defmodule Routeguide.RouteSummary do - use Protobuf - - @type t :: %__MODULE__{ - point_count: integer, - feature_count: integer, - distance: integer, - elapsed_time: integer - } - defstruct [:point_count, :feature_count, :distance, :elapsed_time] - - field :point_count, 1, optional: true, type: :int32 - field :feature_count, 2, optional: true, type: :int32 - field :distance, 3, optional: true, type: :int32 - field :elapsed_time, 4, optional: true, type: :int32 -end - -defmodule Routeguide.RouteGuide.Service do - use GRPC.Service, name: "routeguide.RouteGuide" - - rpc :GetFeature, Routeguide.Point, Routeguide.Feature - rpc :ListFeatures, Routeguide.Rectangle, stream(Routeguide.Feature) - rpc :RecordRoute, stream(Routeguide.Point), Routeguide.RouteSummary - rpc :RouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) - rpc :AsyncRouteChat, stream(Routeguide.RouteNote), stream(Routeguide.RouteNote) -end - -defmodule Routeguide.RouteGuide.Stub do - use GRPC.Stub, service: Routeguide.RouteGuide.Service -end diff --git a/test/support/route_guide_transcode.pb.ex b/test/support/route_guide_transcode.pb.ex new file mode 100644 index 000000000..1079307f5 --- /dev/null +++ b/test/support/route_guide_transcode.pb.ex @@ -0,0 +1,46 @@ +defmodule RouteguideTranscode.Point do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :latitude, 1, type: :int32 + field :longitude, 2, type: :int32 +end + +defmodule RouteguideTranscode.Rectangle do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :lo, 1, type: RouteguideTranscode.Point + field :hi, 2, type: RouteguideTranscode.Point +end + +defmodule RouteguideTranscode.Feature do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :name, 1, type: :string + field :location, 2, type: RouteguideTranscode.Point +end + +defmodule RouteguideTranscode.RouteNote do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :location, 1, type: RouteguideTranscode.Point + field :message, 2, type: :string +end + +defmodule RouteguideTranscode.RouteSummary do + @moduledoc false + + use Protobuf, protoc_gen_elixir_version: "0.11.0", syntax: :proto3 + + field :point_count, 1, type: :int32, json_name: "pointCount" + field :feature_count, 2, type: :int32, json_name: "featureCount" + field :distance, 3, type: :int32 + field :elapsed_time, 4, type: :int32, json_name: "elapsedTime" +end diff --git a/test/support/test_adapter.exs b/test/support/test_adapter.exs index 38453434c..4fe3c161b 100644 --- a/test/support/test_adapter.exs +++ b/test/support/test_adapter.exs @@ -1,8 +1,19 @@ defmodule GRPC.Test.ClientAdapter do + @behaviour GRPC.Client.Adapter + def connect(channel, _opts), do: {:ok, channel} + def disconnect(channel), do: {:ok, channel} + def send_request(stream, _message, _opts), do: stream + def receive_data(_stream, _opts), do: {:ok, nil} + def send_data(stream, _message, _opts), do: stream + def send_headers(stream, _opts), do: stream + def end_stream(stream), do: stream + def cancel(stream), do: stream end defmodule GRPC.Test.ServerAdapter do + @behaviour GRPC.Server.Adapter + def start(s, h, p, opts) do {s, h, p, opts} end @@ -26,4 +37,9 @@ defmodule GRPC.Test.ServerAdapter do def has_sent_headers?(_stream) do false end + + def set_headers(stream, headers) do + send(self(), {:setting_headers, headers}) + stream + end end diff --git a/test/support/transcode_messages.pb.ex b/test/support/transcode_messages.pb.ex new file mode 100644 index 000000000..0a632e695 --- /dev/null +++ b/test/support/transcode_messages.pb.ex @@ -0,0 +1,166 @@ +defmodule Transcode.MessageOut do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :response, 1, type: Transcode.Message +end + +defmodule Transcode.GetMessageRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string +end + +defmodule Transcode.Message do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :name, 1, type: :string + field :text, 2, type: :string +end + +defmodule Transcode.NestedMessageRequest do + @moduledoc false + use Protobuf, protoc_gen_elixir_version: "0.14.1", syntax: :proto3 + + field :message, 1, type: Transcode.GetMessageRequest +end + +defmodule Transcode.Messaging.Service do + @moduledoc false + + use GRPC.Service, name: "transcode.Messaging", protoc_gen_elixir_version: "0.14.1" + + rpc(:GetMessage, Transcode.GetMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/messages/{name}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:StreamMessages, Transcode.GetMessageRequest, stream(Transcode.Message), %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/messages/stream/{name}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:GetMessageWithSubPath, Transcode.GetMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/{name=}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:GetMessageWithQuery, Transcode.GetMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/messages"}, + __unknown_fields__: [] + } + } + }) + + rpc(:GetMessageWithFieldPath, Transcode.NestedMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/messages/fieldpath/{message.name}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:CreateMessage, Transcode.Message, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "*", + additional_bindings: [], + response_body: "", + pattern: {:post, "/v1/messages"}, + __unknown_fields__: [] + } + } + }) + + rpc(:GetMessageWithResponseBody, Transcode.GetMessageRequest, Transcode.MessageOut, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "response", + pattern: {:get, "/v1/messages/response_body/{name}"}, + __unknown_fields__: [] + } + } + }) + + rpc(:CreateMessageWithNestedBody, Transcode.NestedMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "message", + additional_bindings: [], + response_body: "", + pattern: {:post, "/v1/messages/nested"}, + __unknown_fields__: [] + } + } + }) + + rpc(:GetMessageWithSubpathQuery, Transcode.NestedMessageRequest, Transcode.Message, %{ + http: %{ + type: Google.Api.PbExtension, + value: %Google.Api.HttpRule{ + selector: "", + body: "", + additional_bindings: [], + response_body: "", + pattern: {:get, "/v1/messages/nested"}, + __unknown_fields__: [] + } + } + }) +end + +defmodule Transcode.Messaging.Stub do + @moduledoc false + + use GRPC.Stub, service: Transcode.Messaging.Service +end diff --git a/test/support/transcode_messages.proto b/test/support/transcode_messages.proto new file mode 100644 index 000000000..273e3127d --- /dev/null +++ b/test/support/transcode_messages.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +import "google/api/annotations.proto"; + +package transcode; + +service Messaging { + rpc GetMessage(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/messages/{name}" + }; + } + + rpc StreamMessages(GetMessageRequest) returns (stream Message) { + option (google.api.http) = { + get: "/v1/messages/stream/{name}" + }; + } + + rpc GetMessageWithSubPath(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/{name=}" + }; + } + + rpc GetMessageWithQuery(GetMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/messages" + }; + } + + rpc GetMessageWithFieldPath(NestedMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/messages/fieldpath/{message.name}" + }; + } + + rpc CreateMessage(Message) returns (Message) { + option (google.api.http) = { + post: "/v1/messages" + body: "*" + }; + } + + rpc GetMessageWithResponseBody(GetMessageRequest) returns (MessageOut) { + option (google.api.http) = { + get: "/v1/messages/response_body/{name}", + response_body: "response" + }; + } + + rpc CreateMessageWithNestedBody(NestedMessageRequest) returns (Message) { + option (google.api.http) = { + post: "/v1/messages/nested", + body: "message" + }; + } + + rpc GetMessageWithSubpathQuery(NestedMessageRequest) returns (Message) { + option (google.api.http) = { + get: "/v1/messages/nested" + }; + } +} + +message MessageOut { + Message response = 1; +} + +message GetMessageRequest { + string name = 1; // Mapped to URL path. +} + +message Message { + string name = 1; + string text = 2; +} + +message NestedMessageRequest { + GetMessageRequest message = 1; +} diff --git a/test/test_helper.exs b/test/test_helper.exs index 4c5aef468..805a2a642 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,3 +1,10 @@ Code.require_file("./support/test_adapter.exs", __DIR__) -ExUnit.start() +codecs = [ + GRPC.Codec.Erlpack, + GRPC.Codec.WebText, + GRPC.Codec.Proto +] + +Enum.each(codecs, &Code.ensure_loaded/1) +ExUnit.start(capture_log: true)