Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation #1

Merged
merged 20 commits into from Feb 17, 2017
Merged

Initial implementation #1

merged 20 commits into from Feb 17, 2017

Conversation

rslota
Copy link
Contributor

@rslota rslota commented Feb 9, 2017

This PR introduces... well, this project's implementation. This is, I would say, the first draft. It works and it has integration tests (100% coverage, and I hope it will stay this way). There are also some docs (mostly README.md) and several handy integrations (travis, credo, coveralls.io etc.).

Even though there is Dockerfile on my TODO list, I think I'll skip it and we may add it later on. It's not needed, just very convenient.

TODO:

  • tests (100% coverage :))
  • coveralls.io integration
  • dialyxir integration
  • credo integration (linter)
  • travis build integration (coverage + dialyzer + code linter)
  • Code docs / specs
  • Write README.md
  • Dockerfile
  • Change SVG bages in README.md to point to master branch just before merging

@rslota rslota force-pushed the initial_implementation branch 2 times, most recently from fd2316c to b2865c3 Compare February 9, 2017 14:13
@rslota rslota force-pushed the initial_implementation branch 3 times, most recently from cd2fe3c to 5cf4941 Compare February 9, 2017 15:51
Copy link
Contributor

@michalwski michalwski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is simple, indeed :) I have some comments to this PR.

Also in a following PR I think we should add some metrics counting number of requests from clients, number of failed and succeeded requests to FCM and APNS. Elapsed time measurements for the whole request and specific requests to providers may prove useful on production as well.

using: :path
],
https: [
ip: {127, 0, 0, 1},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about {0, 0, 0, 0} by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think its a good idea. In many cases this project will be used on same host as MongooseIM. Also if somebody uses it on different host, he will notice that he has to change configuration because REST API won't work without it. But with {0, 0, 0, 0} user may not notice that his service is open for attacks, because it will always work this way, even if such configuration is not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, thanks for the explanation.

@@ -29,6 +42,19 @@ defmodule MongoosePush.Mixfile do
#
# Type "mix help deps" for more examples and options
defp deps do
[]
[
{:pigeon, git: "https://github.com/rslota/pigeon.git", tag: "6d1e4e3"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see rslota/pigeon is a fork from cstar/pigeon. Are you @rslota going to send a PR to upstream?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can try, but there are some changes that the author may not like. Author of this project had much different plan when comes to pooling.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to convince him :)

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can all these *.pem files be generated on demand? For example when the project is first compiled or run?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They can be generated but it adds one and only one additional step to building the project. If I leave those example certs in repo, there not need to have openssl while running the project and there not need for Makefile.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe openssl is required in order to provide HTTPs from Elixir node. This probably means that openssl will have to be installed anyway. My biggest concern is with such files included in the repo, users may start using them without regenerating. For such a simple project which most probably we'll be used internally fresh certs and keys may not be mission critical. It is still sth which lights a warning in my brain, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, you can now generate those certs via mix certs.dev.

# Skip this setup for jobs that don't run exunit
test "${PRESET}" == "exunit" || exit 0

docker run -d -p 2197:2197 mobify/apns-http2-mock-server
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any chance for a specific version here and in the next container?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want the latest. Always. If the API changes we need to adapt, because our code needs to be compatible with newest APNS / FCM API.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough :)

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 1dae070 on initial_implementation into ** on master**.

Copy link

@mentels mentels left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will continue the review from lib/mongoose_push/api/v1.ex tomorrow.

README.md Outdated

#### Perquisites

* Elixir (http://elixir-lang.org/install.html)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which version of Elixir?

README.md Outdated

## Configuration

The whole configuration is contained in file `config/{prod|dev|test}.exs` depending on with `MIX_ENV` you will be using. You should use `MIX_ENV=prod` for production installations and `MIX_ENV=dev` for you development. Anyway, lets take a look on `config/prod.exs`, part by part.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

on witch -> on which

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for you development -> ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

your* :)

After this step you may try to run the service via:
```bash
_build/prod/rel/mongoose_push/bin/mongoose_push console
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to be serious, we really need to support releases. I won't accept an OS PR that has on support for releases :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you elaborate? What do you mean by support for releases exactly? This README.md shows how to run this project from the release.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Damn...how could I have missed that? Sorry, of course you do support releases with distillery. Awesome :)

README.md Outdated
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 [https://hexdocs.pm/mongoose_push](https://hexdocs.pm/mongoose_push).
The first level keyword list is a pool definition. You may have several named pools of different sizes and with different configurations. Currently the only reason you may want to do this is that, the `REST` client may switch between them by specifying matching `:mode` in their push request.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"The first level keyword list is a pool definition." - this is not very understandable. Maybe start with "to setup a pool...".

use Mix.Task

@shortdoc "Generate fake certs (placeholders) for HTTPS endpoint and APNS"
@moduledoc """
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@moduledoc is usually put at the very top.

end

defp maybe_gen_cert(cert_file, key_file, common_name) do
unless File.exists?(cert_file) and File.exists?(key_file) do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition is not readable. If I need to think what it means it has to be structured differently. One option would be:

case {File.exists?(cert_file), File.exists?(key_file)} do
    {false, false} -> ...
    _ -> ...
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that readability is very subjective, especially in this case (huh, case :)). This case pattern may be easier to read for erlang devs since in erlang this is the only sane way to write such condition. Elixir makes it easier and in my opinion more readable, so I wanted to exploit that.

But anyway, this is so minor that I'm just gonna change it, I don't really like unless that much :)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can simply write if File.exists? ... and ... do ok else do_stuff() end

@doc """
Hello world.
require Logger
import MongoosePush.Application
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I really discourage these import. Because of that it takes you time to find functions like pools_by_name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thats true, alias seems to be a better idea.

@spec push(String.t, request) :: :ok | {:error, term}
def push(device_id, %{:service => service} = request) do
mode = Map.get(request, :mode, :prod)
[pool | _] = pools_by_mode(service, mode)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the functions on lines 44 and 45 are consructing a worker name. For me this level of abstraction doesn't belong to this function.


@spec push(service, atom | pid, String.t, request) :: :ok | {:error, term}
defp push(:apns, worker, device_id, request) do
raw_notification = prepare_notification(:apns, request)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type of code can be rewritten as:

raw_notification = 
     prepare_notification(:apns, request)
     |> APNS.Notification.new(device_id, request[:topic])
     |> APNS.push([name: worker])
     |> normalize_response(:apns, device_id)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I did write it this way, but credo does not agree with you. Its says that each pipe operator has to start with variable, not expression.

"category" => request[:click_action]
}
end

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, this module should not distinguish between the two types of the backends further than :fcm and :apns. In other words, I believe that the two deserve their own modules like MongoosePush.FCM and MongoosePush.APNS and the specifics of these services should sit in them. Maybe the next PR would address that but I simply don't like it.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 4443b46bd7f8ab43fb0e87daa7d0023fff0d4199 on initial_implementation into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling a4a88a5 on initial_implementation into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling 2dbe39d on initial_implementation into ** on master**.

{:error, reason} ->
conn
|> put_status(500)
|> json(%{:details => inspect reason})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module is beautiful. Simple and readable. One issue though, the inspect from the Kernel module is not to be used to format data that you return to the client. You have to format you data differently, without inspect.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

config = env(service)

config
|> Enum.group_by(&(mode(elem(&1, 1))), &(elem &1, 0))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really readable.

end)

workers
|> List.flatten
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of flattening the list afterwards, you could use Enum.flat_map/2.

true -> :error
false -> :info
end
Logger.log log_level, inspect e
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, you shouldn't be using inspect to format your output data.

@@ -0,0 +1,9 @@
defmodule MongoosePush.Service do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool :)


defp get(path) do
%Response{status_code: status_code} =
HTTPoison.get!("https://localhost:8443" <> path, [], hackney: [:insecure])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you using the insecure hackney option?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this is connection to HTTPS endpoint that is running on self-signed certs generated for tests. Without this option connection would fail due to unknown_ca error.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

end

defp maybe_gen_cert(cert_file, key_file, common_name) do
unless File.exists?(cert_file) and File.exists?(key_file) do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can simply write if File.exists? ... and ... do ok else do_stuff() end

@doc """
Hello world.
require Logger
alias MongoosePush.Application
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this alias. It's misleading as at first glance you don't know if it refers to Elixir.Application or to Elixir.MngoosePush.Application.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, you're right.

@@ -4,19 +4,126 @@ defmodule MongoosePush.Application do
@moduledoc false
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me, the whole pools management doesn't belong to this module. If would prefer just having it separated out to sth like MongoosePush.Pools which would provide some API to get workers names to start then under supervisors and then the other one to select a woker. Even better, I believe, is a solution in which you have a separate supervisors for each type of pool and it knows more details and the the "pools" module that know even more details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pools have been refactored. Please review new implementation :)

pool_config = translate_worker_config(:fcm, pool_config)

Enum.map(1..pool_size(:fcm, pool_name), fn(id) ->
worker_name = worker_name(:fcm, pool_name, id)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To deep... doesn't credo complain about that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No it does not. This is only 2 level in, so I guess its maximum allowed.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling ea9383e42f2b47bb95f0e5746f6ee004c361f651 on initial_implementation into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling fdef967 on initial_implementation into ** on master**.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling fdef967 on initial_implementation into ** on master**.

@@ -0,0 +1,44 @@
defmodule MongoosePush.Pools do
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now this is awesome

setup do
# Validate config/text.exs that is need for this test suite
apns_pools = Keyword.keys(Application.get_env(:mongoose_push, :apns))
4 = length(apns_pools)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That could be reduced to [:prod1, :prod2, :dev1, :dev2] = apns_pools

But is so minor that you don't have to bother yourself.

@coveralls
Copy link

Coverage Status

Changes Unknown when pulling aa07594 on initial_implementation into ** on master**.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants