From 26f2772a3d5e781a8a0e2d0844aa014fc91985b3 Mon Sep 17 00:00:00 2001 From: Nate Anderson Date: Thu, 18 Jul 2024 12:36:35 -0700 Subject: [PATCH 1/2] chore: add agent and runtime-version metadata Add agent and runtime-version metadata to the first call made by a data and control client. It uses the process dictionary to manage the state about whether it has sent the data already. Add tests that check that the agent data is onlt sent once. --- .../momento/internal/scs_control_client.ex | 38 +++++++++++-- src/lib/momento/internal/scs_data_client.ex | 53 +++++++++++++++---- src/mix.exs | 3 +- src/mix.lock | 2 + .../internal/scs_control_client_test.exs | 38 +++++++++++++ .../momento/internal/scs_data_client_test.exs | 38 +++++++++++++ 6 files changed, 158 insertions(+), 14 deletions(-) create mode 100644 src/test/momento/internal/scs_control_client_test.exs create mode 100644 src/test/momento/internal/scs_data_client_test.exs diff --git a/src/lib/momento/internal/scs_control_client.ex b/src/lib/momento/internal/scs_control_client.ex index 902b62d..762e47c 100644 --- a/src/lib/momento/internal/scs_control_client.ex +++ b/src/lib/momento/internal/scs_control_client.ex @@ -39,7 +39,7 @@ defmodule Momento.Internal.ScsControlClient do @spec list_caches(client :: t()) :: Momento.Responses.ListCaches.t() def list_caches(client) do - metadata = %{Authorization: client.auth_token} + metadata = create_metadata(client) list_caches_request = %Momento.Protos.ControlClient.ListCachesRequest{} case Momento.Protos.ControlClient.ScsControl.Stub.list_caches( @@ -60,7 +60,7 @@ defmodule Momento.Internal.ScsControlClient do @spec create_cache(client :: t(), cache_name :: String.t()) :: Momento.Responses.CreateCache.t() def create_cache(client, cache_name) do - metadata = %{Authorization: client.auth_token} + metadata = create_metadata(client) create_cache_request = %Momento.Protos.ControlClient.CreateCacheRequest{ cache_name: cache_name @@ -88,7 +88,7 @@ defmodule Momento.Internal.ScsControlClient do @spec delete_cache(client :: t(), cache_name :: String.t()) :: Momento.Responses.DeleteCache.t() def delete_cache(client, cache_name) do - metadata = %{Authorization: client.auth_token} + metadata = create_metadata(client) delete_cache_request = %Momento.Protos.ControlClient.DeleteCacheRequest{ cache_name: cache_name @@ -108,4 +108,36 @@ defmodule Momento.Internal.ScsControlClient do end end end + + @agent_data_key "__#{__MODULE__}_AGENT_DATA_SENT__" + + defp should_send_agent_data? do + :erlang.get(@agent_data_key) == :undefined + end + + @spec create_metadata(t()) :: %{required(String.t()) => String.t()} + defp create_metadata(client) do + base_metadata = %{ + "authorization" => client.auth_token + } + + if should_send_agent_data?() do + :erlang.put(@agent_data_key, true) + + Map.merge(base_metadata, %{ + "agent" => "elixir:cache:" <> get_library_version(), + "runtime-version" => System.version() + }) + else + base_metadata + end + end + + @spec get_library_version() :: String.t() + defp get_library_version do + case Application.spec(:gomomento, :vsn) do + nil -> "unknown" + version -> to_string(version) + end + end end diff --git a/src/lib/momento/internal/scs_data_client.ex b/src/lib/momento/internal/scs_data_client.ex index a39ca53..6e3b78c 100644 --- a/src/lib/momento/internal/scs_data_client.ex +++ b/src/lib/momento/internal/scs_data_client.ex @@ -48,7 +48,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), {:ok, ttl_milliseconds} <- get_ttl_milliseconds(ttl_seconds) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) set_request = %Momento.Protos.CacheClient.SetRequest{ cache_key: key, @@ -75,7 +75,7 @@ defmodule Momento.Internal.ScsDataClient do def get(data_client, cache_name, key) do with :ok <- validate_cache_name(cache_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) get_request = %Momento.Protos.CacheClient.GetRequest{cache_key: key} @@ -104,7 +104,7 @@ defmodule Momento.Internal.ScsDataClient do def delete(data_client, cache_name, key) do with :ok <- validate_cache_name(cache_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) delete_request = %Momento.Protos.CacheClient.DeleteRequest{cache_key: key} @@ -141,7 +141,7 @@ defmodule Momento.Internal.ScsDataClient do {:ok, ttl_milliseconds} <- get_ttl_milliseconds(collection_ttl), {:ok, transformed_elements} <- transform_sorted_set_elements(elements) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) sorted_set_put_request = %Momento.Protos.CacheClient.SortedSetPutRequest{ set_name: sorted_set_name, @@ -211,7 +211,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), :ok <- validate_sorted_set_name(sorted_set_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) start_index = case start_rank do @@ -298,7 +298,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), :ok <- validate_sorted_set_name(sorted_set_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) request_min_score = case min_score do @@ -405,7 +405,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), :ok <- validate_sorted_set_name(sorted_set_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) remove_request = %Momento.Protos.CacheClient.SortedSetRemoveRequest{ set_name: sorted_set_name, @@ -449,7 +449,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), :ok <- validate_sorted_set_name(sorted_set_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) request_order = case sort_order do @@ -571,7 +571,7 @@ defmodule Momento.Internal.ScsDataClient do with :ok <- validate_cache_name(cache_name), :ok <- validate_sorted_set_name(sorted_set_name) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) get_scores_request = %Momento.Protos.CacheClient.SortedSetGetScoreRequest{ set_name: sorted_set_name, @@ -642,7 +642,7 @@ defmodule Momento.Internal.ScsDataClient do :ok <- validate_sorted_set_name(sorted_set_name), {:ok, ttl_milliseconds} <- get_ttl_milliseconds(collection_ttl) do try do - metadata = %{cache: cache_name, Authorization: data_client.auth_token} + metadata = create_metadata(cache_name, data_client) increment_request = %Momento.Protos.CacheClient.SortedSetIncrementRequest{ set_name: sorted_set_name, @@ -687,4 +687,37 @@ defmodule Momento.Internal.ScsDataClient do defp get_ttl_milliseconds(ttl), do: {:error, Momento.Error.invalid_argument("Unable to parse TTL from #{ttl}")} + + @agent_data_key "__#{__MODULE__}_AGENT_DATA_SENT__" + + defp should_send_agent_data? do + :erlang.get(@agent_data_key) == :undefined + end + + @spec create_metadata(String.t(), t()) :: %{required(String.t()) => String.t()} + defp create_metadata(cache_name, data_client) do + base_metadata = %{ + "cache" => cache_name, + "authorization" => data_client.auth_token + } + + if should_send_agent_data?() do + :erlang.put(@agent_data_key, true) + + Map.merge(base_metadata, %{ + "agent" => "elixir:cache:" <> get_library_version(), + "runtime-version" => System.version() + }) + else + base_metadata + end + end + + @spec get_library_version() :: String.t() + defp get_library_version do + case Application.spec(:gomomento, :vsn) do + nil -> "unknown" + version -> to_string(version) + end + end end diff --git a/src/mix.exs b/src/mix.exs index 46d485b..2261335 100644 --- a/src/mix.exs +++ b/src/mix.exs @@ -38,7 +38,8 @@ defmodule Momento.MixProject do {:google_protos, "~> 0.3"}, {:joken, "~> 2.5"}, {:jason, "~> 1.4"}, - {:tls_certificate_check, "~> 1.19"} + {:tls_certificate_check, "~> 1.19"}, + {:mock, "~> 0.3.8", only: :test} ] end diff --git a/src/mix.lock b/src/mix.lock index 1dd5ac7..82ca53c 100644 --- a/src/mix.lock +++ b/src/mix.lock @@ -14,6 +14,8 @@ "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"}, "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"}, + "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, + "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"}, "protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"}, "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, diff --git a/src/test/momento/internal/scs_control_client_test.exs b/src/test/momento/internal/scs_control_client_test.exs new file mode 100644 index 0000000..338559a --- /dev/null +++ b/src/test/momento/internal/scs_control_client_test.exs @@ -0,0 +1,38 @@ +defmodule Momento.Internal.ScsControlClientTest do + use ExUnit.Case, async: false + import Mock + + alias Momento.Internal.ScsControlClient + alias Momento.Protos.ControlClient.ScsControl.Stub + + test "agent metadata is only sent on the first call" do + fake_channel = :fake_channel + + client = %ScsControlClient{ + auth_token: "test_token", + channel: fake_channel + } + + with_mock Stub, [:passthrough], + create_cache: fn ^fake_channel, _request, options -> + metadata = Keyword.get(options, :metadata, %{}) + send(self(), {:grpc_call, metadata}) + {:ok, %{}} + end do + ScsControlClient.create_cache(client, "test_cache") + assert_received {:grpc_call, metadata} + assert Map.has_key?(metadata, "agent") + assert Map.has_key?(metadata, "runtime-version") + + ScsControlClient.create_cache(client, "test_cache") + assert_received {:grpc_call, metadata} + refute Map.has_key?(metadata, "agent") + refute Map.has_key?(metadata, "runtime-version") + + ScsControlClient.create_cache(client, "test_cache") + assert_received {:grpc_call, metadata} + refute Map.has_key?(metadata, "agent") + refute Map.has_key?(metadata, "runtime-version") + end + end +end diff --git a/src/test/momento/internal/scs_data_client_test.exs b/src/test/momento/internal/scs_data_client_test.exs new file mode 100644 index 0000000..3e1c7a7 --- /dev/null +++ b/src/test/momento/internal/scs_data_client_test.exs @@ -0,0 +1,38 @@ +defmodule Momento.Internal.ScsDataClientTest do + use ExUnit.Case, async: false + import Mock + + alias Momento.Internal.ScsDataClient + alias Momento.Protos.CacheClient.Scs.Stub + + test "agent metadata is only sent on the first call" do + fake_channel = :fake_channel + + client = %ScsDataClient{ + auth_token: "test_token", + channel: fake_channel + } + + with_mock Stub, [:passthrough], + set: fn ^fake_channel, _request, options -> + metadata = Keyword.get(options, :metadata, %{}) + send(self(), {:grpc_call, metadata}) + {:ok, %{}} + end do + ScsDataClient.set(client, "test_cache", "key1", "value1", 60) + assert_received {:grpc_call, metadata} + assert Map.has_key?(metadata, "agent") + assert Map.has_key?(metadata, "runtime-version") + + ScsDataClient.set(client, "test_cache", "key2", "value2", 60) + assert_received {:grpc_call, metadata} + refute Map.has_key?(metadata, "agent") + refute Map.has_key?(metadata, "runtime-version") + + ScsDataClient.set(client, "test_cache", "key3", "value3", 60) + assert_received {:grpc_call, metadata} + refute Map.has_key?(metadata, "agent") + refute Map.has_key?(metadata, "runtime-version") + end + end +end From 860ef3920ca0a2d7181fb5d3b892939634ae2098 Mon Sep 17 00:00:00 2001 From: Nate Anderson Date: Fri, 16 Aug 2024 16:49:11 -0700 Subject: [PATCH 2/2] Add examples for what agent and runtime-version look like --- src/lib/momento/internal/scs_control_client.ex | 2 ++ src/lib/momento/internal/scs_data_client.ex | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/lib/momento/internal/scs_control_client.ex b/src/lib/momento/internal/scs_control_client.ex index 762e47c..f0d088f 100644 --- a/src/lib/momento/internal/scs_control_client.ex +++ b/src/lib/momento/internal/scs_control_client.ex @@ -125,7 +125,9 @@ defmodule Momento.Internal.ScsControlClient do :erlang.put(@agent_data_key, true) Map.merge(base_metadata, %{ + # example agent: "elixir:cache:0.6.6" "agent" => "elixir:cache:" <> get_library_version(), + # example runtime-version: "1.16.2" "runtime-version" => System.version() }) else diff --git a/src/lib/momento/internal/scs_data_client.ex b/src/lib/momento/internal/scs_data_client.ex index 6e3b78c..071d5dc 100644 --- a/src/lib/momento/internal/scs_data_client.ex +++ b/src/lib/momento/internal/scs_data_client.ex @@ -704,8 +704,14 @@ defmodule Momento.Internal.ScsDataClient do if should_send_agent_data?() do :erlang.put(@agent_data_key, true) + IO.puts("Versions:") + IO.puts(get_library_version()) + IO.puts(System.version()) + Map.merge(base_metadata, %{ + # example agent: "elixir:cache:0.6.6" "agent" => "elixir:cache:" <> get_library_version(), + # example runtime-version: "1.16.2" "runtime-version" => System.version() }) else