From 8711668e9be2a05cae571fc36b480288bf0420a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20Makie=C5=82a?= Date: Fri, 13 Sep 2019 17:19:51 +0200 Subject: [PATCH] Pool tags support * Added support for tags in pool configurations * Added support for selecting pools based on tags * Added tests for checking tags' behaviour * Updated README * Fixed issue with no matching pools in FCM * Updated Sparrow ref * Updated `FCM.Pool.Supervisor` module doc --- README.md | 3 + config/test.exs | 13 +- lib/mongoose_push.ex | 28 +-- lib/mongoose_push/service/apns.ex | 6 +- lib/mongoose_push/service/apns/supervisor.ex | 6 +- lib/mongoose_push/service/fcm.ex | 6 +- .../service/fcm/pool/supervisor.ex | 27 +-- mix.exs | 2 +- mix.lock | 4 +- test/mongoose_push_test.exs | 159 ++++++++++++++++++ 10 files changed, 221 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 5340e47f..e3970614 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ Each `FCM` pool may be configured by setting the following fields: * **pool_size** (*required*) - maximum number of used `HTTP/2` connections to google's service * **mode** (*either `:prod` or `:dev`*) - pool's mode. The `HTTP` client may select pool used to push a notification by specifying matching option in the request * **endpoint** (*optional*) - URL override for `FCM` service. Useful mainly in tests +* **tags** (*optional*) - a list of tags. Used when choosing pool to match request tags when sending a notification. More details: https://github.com/esl/sparrow#tags You may entirely skip the `FCM` config entry to disable `FCM` support. @@ -202,6 +203,7 @@ Each `APNS` pool may be configured by setting the following fields: * **mode** (*either `:prod` or `:dev`*) - pool's mode. The `HTTP` client may select pool used to push a notification by specifying matching option in the request * **endpoint** (*optional*) - URL override for `APNS` service. Useful mainly in tests * **use_2197** (*optional `true` or `false`*) - whether use alternative port for `APNS`: 2197 +* **tags** (*optional*) - a list of tags. Used when choosing pool to match request tags when sending a notification. More details: https://github.com/esl/sparrow#tags You may entirely skip the `APNS` config entry to disable `APNS` support. @@ -250,6 +252,7 @@ The full list of options contains the following: * **time_to_live** (*optional*) - Maximum lifespan of an FCM notification. For more details, please, refer to [the official FCM documentation](https://firebase.google.com/docs/cloud-messaging/concept-options#ttl). * **mutable_content** (*optional*, `true` / `false` (default)) - Only applicable to APNS. Sets "mutable-content=1" in APNS payload. * **topic** (*optional*, `APNS` specific) - if APNS certificate configured in `MongoosePush` allows for multiple applications, this field selects the application. Please refer to `APNS` documentation for more datails +* **tags** (*optional*) - a list of tags used to choose a pool with matching tags. To see how tags work read: https://github.com/esl/sparrow#tags * **data** (*optional*) - custom JSON structure sent to the target device. For `APNS`, all keys form this stucture are merged into highest level APS message (the one that holds 'aps' key), while for `FCM` the whole `data` json stucture is sent as FCM's `data payload` along with `notification`. * **alert** (*optional*) - JSON stucture that if provided will send non-silent notification with the following fields: * **body** (*required*) - text body of notification diff --git a/config/test.exs b/config/test.exs index a5b6fb25..ed658e31 100644 --- a/config/test.exs +++ b/config/test.exs @@ -21,12 +21,21 @@ config :maru, MongoosePush.Router, config :mongoose_push, fcm: [ - default: [ + pool1: [ appfile: "priv/fcm/token.json", endpoint: "localhost", pool_size: 5, mode: :prod, - port: 4000 + port: 4000, + tags: [:I, :am, :your, :father] + ], + pool2: [ + appfile: "priv/fcm/token.json", + endpoint: "localhost", + pool_size: 3, + mode: :dev, + port: 4000, + tags: [:these, :are, :not] ] ] diff --git a/lib/mongoose_push.ex b/lib/mongoose_push.ex index ae27a786..af8a89c2 100644 --- a/lib/mongoose_push.ex +++ b/lib/mongoose_push.ex @@ -63,16 +63,24 @@ defmodule MongoosePush do def push(device_id, %{:service => service} = request) do mode = Map.get(request, :mode, :prod) module = MongoosePush.Application.services()[service] - pool = module.choose_pool(mode) - # Just make sure both data and alert keys exist for convenience (but may be nil) - request = - request - |> Map.put(:alert, request[:alert]) - |> Map.put(:data, request[:data]) - - notification = module.prepare_notification(device_id, request, pool) - opts = [timeout: 60_000] - {time, push_result} = :timer.tc(module, :push, [notification, device_id, pool, opts]) + tags = Map.get(request, :tags, []) + pool = module.choose_pool(mode, tags) + + {time, push_result} = + if pool == nil do + Logger.error(~s"No pool matching mode=#{mode} and tags=#{inspect(tags)}") + {0, {:error, :no_matching_pool}} + else + request = + request + |> Map.put(:alert, request[:alert]) + |> Map.put(:data, request[:data]) + + notification = module.prepare_notification(device_id, request, pool) + opts = [timeout: 60_000] + + :timer.tc(module, :push, [notification, device_id, pool, opts]) + end push_result |> Metrics.update(:spiral, [:push, service, mode]) diff --git a/lib/mongoose_push/service/apns.ex b/lib/mongoose_push/service/apns.ex index 13298de2..52732adf 100644 --- a/lib/mongoose_push/service/apns.ex +++ b/lib/mongoose_push/service/apns.ex @@ -59,9 +59,9 @@ defmodule MongoosePush.Service.APNS do {MongoosePush.Service.APNS.Supervisor, pool_configs} end - @spec choose_pool(MongoosePush.mode()) :: Application.pool_name() | nil - def choose_pool(mode) do - Sparrow.PoolsWarden.choose_pool({:apns, mode}) + @spec choose_pool(MongoosePush.mode(), [any]) :: Application.pool_name() | nil + def choose_pool(mode, tags \\ []) do + Sparrow.PoolsWarden.choose_pool({:apns, mode}, tags) end defp maybe(notification, :add_mutable_content, true), diff --git a/lib/mongoose_push/service/apns/supervisor.ex b/lib/mongoose_push/service/apns/supervisor.ex index d1a3cf7e..d7449fbc 100644 --- a/lib/mongoose_push/service/apns/supervisor.ex +++ b/lib/mongoose_push/service/apns/supervisor.ex @@ -110,7 +110,8 @@ defmodule MongoosePush.Service.APNS.Supervisor do worker_num: pool_size, endpoint: pool_config[:endpoint] || endpoint_mode, port: port, - pool_name: pool_name + pool_name: pool_name, + tags: pool_config[:tags] ] |> Enum.filter(fn {_key, value} -> !is_nil(value) end) @@ -145,7 +146,8 @@ defmodule MongoosePush.Service.APNS.Supervisor do endpoint: pool_config[:endpoint] || endpoint_mode, port: port, pool_name: pool_name, - token_id: token_id + token_id: token_id, + tags: pool_config[:tags] ] |> Enum.filter(fn {_key, value} -> !is_nil(value) end) diff --git a/lib/mongoose_push/service/fcm.ex b/lib/mongoose_push/service/fcm.ex index 2e458737..b77a8d39 100644 --- a/lib/mongoose_push/service/fcm.ex +++ b/lib/mongoose_push/service/fcm.ex @@ -64,9 +64,9 @@ defmodule MongoosePush.Service.FCM do {PoolSupervisor, pools_configs} end - @spec choose_pool(MongoosePush.mode()) :: Application.pool_name() | nil - def choose_pool(mode) do - Sparrow.PoolsWarden.choose_pool(:fcm, [mode]) + @spec choose_pool(MongoosePush.mode(), [any]) :: Application.pool_name() | nil + def choose_pool(mode, tags \\ []) do + Sparrow.PoolsWarden.choose_pool(:fcm, [mode | tags]) end defp maybe(notification, _function, nil), do: notification diff --git a/lib/mongoose_push/service/fcm/pool/supervisor.ex b/lib/mongoose_push/service/fcm/pool/supervisor.ex index a6671b15..3635433e 100644 --- a/lib/mongoose_push/service/fcm/pool/supervisor.ex +++ b/lib/mongoose_push/service/fcm/pool/supervisor.ex @@ -1,6 +1,6 @@ defmodule MongoosePush.Service.FCM.Pool.Supervisor do @moduledoc """ - This module is a basic FCM pool supervisor that has a list of active workers - temporary solution before migrating to Sparrow with its own supervisors + This module is responsible for setting up Sparrow's FCM Supervisor """ use Supervisor, id: :fcm_pool_supervisor require Logger @@ -34,16 +34,23 @@ defmodule MongoosePush.Service.FCM.Pool.Supervisor do end) end - defp convert_pool_to_sparrow({_pool_name, pool_config}) do - token_path = {:path_to_json, pool_config[:appfile]} - endpoint = {:endpoint, pool_config[:endpoint] || @default_endpoint} - port = {:port, pool_config[:port] || @default_port} - pool_size = {:worker_num, pool_config[:pool_size]} - + defp convert_pool_to_sparrow({pool_name, pool_config}) do + token_path = pool_config[:appfile] + endpoint = pool_config[:endpoint] || @default_endpoint + port = pool_config[:port] || @default_port + pool_size = pool_config[:pool_size] + raw_tags = pool_config[:tags] || [] # mode has to be either `prod` or `dev`, for now we pass it in form of a tag - tags = {:tags, [pool_config[:mode]]} - - [token_path, endpoint, port, pool_size, tags] + tags = [pool_config[:mode] | raw_tags] + + [ + path_to_json: token_path, + endpoint: endpoint, + port: port, + worker_num: pool_size, + tags: tags, + pool_name: pool_name + ] |> Enum.filter(fn {_key, value} -> !is_nil(value) end) end end diff --git a/mix.exs b/mix.exs index b9f61e1c..14ef53a2 100644 --- a/mix.exs +++ b/mix.exs @@ -27,7 +27,7 @@ defmodule MongoosePush.Mixfile do defp deps do [ {:chatterbox, github: "joedevivo/chatterbox", ref: "ff0c2e0", override: true}, - {:sparrow, github: "esl/sparrow", ref: "358a816b913362daa99f126ceb35b172aa511044"}, + {:sparrow, github: "esl/sparrow", ref: "ed31463f03e83d227d5b6af5c1379a8ab6a551b2"}, {:maru, github: "rslota/maru", ref: "54fc038", override: true}, {:cowboy, "~> 2.3", override: true}, {:jason, "~> 1.0"}, diff --git a/mix.lock b/mix.lock index 5e510570..9ffeebd4 100644 --- a/mix.lock +++ b/mix.lock @@ -21,7 +21,7 @@ "exometer_report_graphite": {:git, "https://github.com/esl/exometer_report_graphite.git", "264dd7bcbadbd7febcd43917302251286c88b681", []}, "folsom": {:hex, :folsom, "0.8.5", "94a027b56fe84feed264f9b33cb4c6ac9a801fad84b87dbda0836ce83c3b8d69", [:rebar3], [{:bear, "0.8.5", [hex: :bear, repo: "hexpm", optional: false]}], "hexpm"}, "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"}, - "goth": {:hex, :goth, "0.8.2", "edd1359f4e612266188a6837fcb562626403458398c6ba36d6f8c88a14075366", [:mix], [{:httpoison, "~> 0.11 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:json_web_token, "~> 0.2.10", [hex: :json_web_token, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "goth": {:hex, :goth, "1.1.0", "85977656822e54217bc0472666f1ce15dc3921495ef5f4f0774ef15503bae207", [:mix], [{:httpoison, "~> 0.11 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.11.0", "4951ee019df102492dabba66a09e305f61919a8a183a7860236c0fde586134b6", [:rebar3], [{:certifi, "2.0.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hpack": {:git, "https://github.com/joedevivo/hpack.git", "6b58b6231e9b6ab83096715120578976f72f4f7c", [tag: "0.2.3"]}, "httpoison": {:hex, :httpoison, "0.13.0", "bfaf44d9f133a6599886720f3937a7699466d23bb0cd7a88b6ba011f53c6f562", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, @@ -51,7 +51,7 @@ "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "quixir": {:hex, :quixir, "0.9.3", "f01c37386b9e1d0526f01a8734a6d7884af294a0ec360f05c24c7171d74632bd", [:mix], [{:pollution, "~> 0.9.2", [hex: :pollution, repo: "hexpm", optional: false]}], "hexpm"}, "ranch": {:hex, :ranch, "1.4.0", "10272f95da79340fa7e8774ba7930b901713d272905d0012b06ca6d994f8826b", [:rebar3], [], "hexpm"}, - "sparrow": {:git, "https://github.com/esl/sparrow.git", "358a816b913362daa99f126ceb35b172aa511044", [ref: "358a816b913362daa99f126ceb35b172aa511044"]}, + "sparrow": {:git, "https://github.com/esl/sparrow.git", "ed31463f03e83d227d5b6af5c1379a8ab6a551b2", [ref: "ed31463f03e83d227d5b6af5c1379a8ab6a551b2"]}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, diff --git a/test/mongoose_push_test.exs b/test/mongoose_push_test.exs index b56d766d..10eb7a02 100644 --- a/test/mongoose_push_test.exs +++ b/test/mongoose_push_test.exs @@ -389,6 +389,165 @@ defmodule MongoosePushTest do TestHelper.reload_app() end + describe "tagged pools" do + setup do + apns_config = [ + dev1: [ + auth: %{ + type: :token, + key_id: "fake_key", + team_id: "fake_team", + p8_file_path: "priv/apns/token.p8" + }, + endpoint: "localhost", + mode: :dev, + use_2197: true, + pool_size: 3, + default_topic: "dev_topic1", + tags: [:tag1, :tag2] + ], + dev2: [ + auth: %{ + type: :token, + key_id: "fake_key", + team_id: "fake_team", + p8_file_path: "priv/apns/token.p8" + }, + endpoint: "localhost", + mode: :dev, + use_2197: true, + pool_size: 3, + default_topic: "prod_topic1", + tags: [:tag2, :tag3] + ], + prod1: [ + auth: %{ + type: :token, + key_id: "fake_key", + team_id: "fake_team", + p8_file_path: "priv/apns/token.p8" + }, + endpoint: "localhost", + mode: :prod, + use_2197: true, + pool_size: 3, + default_topic: "dev_topic1", + tags: [:tag1, :tag2] + ], + prod2: [ + auth: %{ + type: :token, + key_id: "fake_key", + team_id: "fake_team", + p8_file_path: "priv/apns/token.p8" + }, + endpoint: "localhost", + mode: :prod, + use_2197: true, + pool_size: 3, + default_topic: "prod_topic1", + tags: [:tag2, :tag3] + ] + ] + + fcm_config = [ + pool1: [ + appfile: "priv/fcm/token.json", + endpoint: "localhost", + pool_size: 5, + mode: :prod, + port: 4000, + tags: [:tag1, :tag2, :tag3] + ], + pool2: [ + appfile: "priv/fcm/token.json", + endpoint: "localhost", + pool_size: 4, + mode: :dev, + port: 4000, + tags: [:tag2, :tag3, :tag4] + ] + ] + + Application.stop(:mongoose_push) + Application.stop(:sparrow) + Application.put_env(:mongoose_push, :apns, apns_config) + Application.put_env(:mongoose_push, :fcm, fcm_config) + {:ok, _} = Application.ensure_all_started(:mongoose_push) + :ok + end + + test "are tagged and chosen correctly" do + assert :dev1 == MongoosePush.Service.APNS.choose_pool(:dev, [:tag1]) + assert :dev1 == MongoosePush.Service.APNS.choose_pool(:dev, [:tag1, :tag2]) + assert :dev2 == MongoosePush.Service.APNS.choose_pool(:dev, [:tag3]) + assert :dev2 == MongoosePush.Service.APNS.choose_pool(:dev, [:tag2, :tag3]) + assert nil == MongoosePush.Service.APNS.choose_pool(:dev, [:tag1, :tag2, :tag3]) + + assert :prod1 == MongoosePush.Service.APNS.choose_pool(:prod, [:tag1]) + assert :prod1 == MongoosePush.Service.APNS.choose_pool(:prod, [:tag1, :tag2]) + assert :prod2 == MongoosePush.Service.APNS.choose_pool(:prod, [:tag3]) + assert :prod2 == MongoosePush.Service.APNS.choose_pool(:prod, [:tag2, :tag3]) + assert nil == MongoosePush.Service.APNS.choose_pool(:prod, [:tag1, :tag2, :tag3]) + + assert :pool1 == MongoosePush.Service.FCM.choose_pool(:prod) + assert :pool1 == MongoosePush.Service.FCM.choose_pool(:prod, [:tag1]) + assert :pool1 == MongoosePush.Service.FCM.choose_pool(:prod, [:tag1, :tag2, :tag3]) + assert :pool2 == MongoosePush.Service.FCM.choose_pool(:dev) + assert :pool2 == MongoosePush.Service.FCM.choose_pool(:dev, [:tag2]) + assert :pool2 == MongoosePush.Service.FCM.choose_pool(:dev, [:tag2, :tag3, :tag4]) + assert nil == MongoosePush.Service.FCM.choose_pool(:prod, [:tag2, :tag3, :tag4]) + assert nil == MongoosePush.Service.FCM.choose_pool(:dev, [:tag1, :tag2, :tag3]) + TestHelper.reload_app() + end + + test "are integrated with APNS" do + notification = %{ + :service => :apns, + :alert => %{ + :title => "title", + :body => "body" + }, + :mode => :prod, + :tags => [:tag2, :tag3], + :data => %{ + "acme1" => "apns1", + "acme2" => "apns2", + "acme3" => "apns3" + } + } + + assert :ok == push(@test_token, notification) + + invalid_notification = + notification + |> Map.replace!(:tags, [:tag1, :tag2, :tag3]) + + assert {:error, :no_matching_pool} == push(@test_token, invalid_notification) + TestHelper.reload_app() + end + + test "are integrated with FCM" do + notification = %{ + :service => :fcm, + :data => %{ + "acme1" => "fcm1", + "acme2" => "fcm2", + "acme3" => "fcm3" + } + } + + assert :ok == push(@test_token, notification) + + invalid_notification = + notification + |> Map.put(:tags, [:tag1, :tag2, :tag3, :tag4]) + + assert {:error, :no_matching_pool} == push(@test_token, invalid_notification) + TestHelper.reload_app() + end + end + defp reset(:apns) do {:ok, conn} = get_connection(:apns) headers = headers("POST", "/reset")