Skip to content

Commit 092da24

Browse files
committed
init
0 parents  commit 092da24

21 files changed

Lines changed: 926 additions & 0 deletions

.formatter.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[
2+
inputs: [
3+
"test/**/*.{ex,exs}",
4+
"lib/**/*.{ex,exs}",
5+
"config/**/*.exs",
6+
"rel/**/*.exs",
7+
"mix.exs"
8+
],
9+
line_length: 80,
10+
]

.gitignore

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Inflex
2+
3+
**TODO: Add description**
4+
5+
## Installation
6+
7+
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
8+
by adding `inflex` to your list of dependencies in `mix.exs`:
9+
10+
```elixir
11+
def deps do
12+
[
13+
{:inflex, "~> 0.1.0"}
14+
]
15+
end
16+
```
17+
18+
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
19+
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
20+
be found at [https://hexdocs.pm/inflex](https://hexdocs.pm/inflex).
21+

config/config.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
use Mix.Config
2+
import_config "#{Mix.env()}.exs"

config/dev.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use Mix.Config

config/test.exs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use Mix.Config
2+
3+
config :logger, level: :warn
4+
5+
config :inflex, Inflex.TestDatabase,
6+
host: "localhost",
7+
udp_port: 8089,
8+
batch_size: 5,
9+
max_queue_size: 100,
10+
flush_interval: 1

lib/application.ex

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
defmodule Inflex.Application do
2+
@moduledoc """
3+
Inflex's per-node registry w/in the library's exported application
4+
5+
Inflex uses `Registry` to support database-name-based dispatch as a primary
6+
design goal was multi-tenancy or sharding support for non-enterprise use
7+
cases.
8+
"""
9+
10+
use Application
11+
12+
def start(_type, _args) do
13+
import Supervisor.Spec
14+
15+
children = [
16+
supervisor(Registry, [:unique, Inflex.Registry])
17+
]
18+
19+
opts = [strategy: :one_for_one, name: Inflex.Application]
20+
21+
Supervisor.start_link(children, opts)
22+
end
23+
end

lib/conn/udp.ex

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Inflex.Conn.UDP do
2+
@moduledoc """
3+
Light, opinionated wrapper around `:gen_udp`
4+
"""
5+
6+
@spec open(
7+
port :: :inet.port_number() | integer(),
8+
udp_opts :: [:gen_udp.option()]
9+
) :: :gen_udp.socket()
10+
@doc """
11+
open a port for UDP with all of the configuration afforded to you by
12+
`:gen_udp.open/2`
13+
"""
14+
def open(port \\ 0, udp_opts \\ [:binary, {:active, false}]) do
15+
{:ok, socket} = :gen_udp.open(port, udp_opts)
16+
socket
17+
end
18+
19+
@spec close(socket :: :gen_udp.socket()) :: :ok
20+
@doc """
21+
when possible, sockets should be closed as a well-behaved program
22+
"""
23+
def close(socket) do
24+
:gen_udp.close(socket)
25+
end
26+
27+
@spec write(
28+
socket :: :gen_udp.socket(),
29+
host :: :inet.ip_address(),
30+
port :: :inet.port_number() | integer(),
31+
String.t()
32+
) :: :ok | {:error, reason :: any()}
33+
@doc """
34+
convert the string to utf-8 binary and send via udp
35+
"""
36+
def write(socket, host, port, payload) do
37+
:gen_udp.send(
38+
socket,
39+
host,
40+
port,
41+
:unicode.characters_to_binary(payload)
42+
)
43+
end
44+
end

lib/database.ex

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
defmodule Inflex.Database do
2+
@moduledoc """
3+
Configuration of a single database's connection pool, queue and supervision
4+
tree. Since InfluxDB uses a single UDP port on the server per database, this
5+
library's goal is to make it easy to do 1:1 mappings of a pool of UDP
6+
connections to that destination port and batching of datapoints.
7+
8+
To get started, create your own module and do something like:
9+
10+
use Inflex.Database, otp_app: :your_app, database: "your_db_name"
11+
12+
In your application, simply add `YourApp.YourInflexDatabase` to the children:
13+
14+
children = [
15+
...,
16+
YourApp.YourInflexDatabase
17+
]
18+
19+
The database name isn't used for the UDP connection in anyway, but it is used
20+
to create database-specific workers. To support querying interfaces and HTTP
21+
based sending, the database would need to be known. To support these features
22+
in the future, the database name is held by all workers in their
23+
configuration.
24+
25+
Where possible, `Inflex.Database.Supervisor` provides sane defaults for
26+
configuration options to make it as close as working out of the box as
27+
possible. A full list of configuration options and their types can be found
28+
there.
29+
"""
30+
31+
defmacro __using__(opts) do
32+
quote bind_quoted: [opts: opts] do
33+
@otp_app Keyword.fetch!(opts, :otp_app)
34+
@database Keyword.fetch!(opts, :database)
35+
36+
alias Inflex.Database.QueueWorker
37+
alias Inflex.Database.Supervisor, as: DBSupervisor
38+
39+
def child_spec(opts) do
40+
%{
41+
id: __MODULE__,
42+
start: {__MODULE__, :start_link, [opts]},
43+
type: :supervisor
44+
}
45+
end
46+
47+
def start_link(opts \\ []) do
48+
opts = Keyword.put(opts, :database, @database)
49+
DBSupervisor.start_link(__MODULE__, @otp_app, opts)
50+
end
51+
52+
def push(%{} = point) do
53+
GenServer.cast(QueueWorker.via_tuple(@database), {:push, point})
54+
end
55+
56+
defoverridable child_spec: 1
57+
end
58+
end
59+
60+
@doc """
61+
queue a single data point (may trigger a batch being sent asynchronously)
62+
"""
63+
@callback push(point :: Inflex.Point.t() | map()) :: :ok
64+
end

lib/database/pool_worker.ex

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
defmodule Inflex.Database.PoolWorker do
2+
@moduledoc """
3+
single worker responsible for sending stats to the configured database
4+
"""
5+
6+
use GenServer
7+
8+
require Logger
9+
10+
alias Inflex.Conn.UDP
11+
alias Inflex.LineProtocol
12+
13+
def child_spec(opts) do
14+
:poolboy.child_spec(:worker, poolboy_config(opts), opts)
15+
end
16+
17+
defp poolboy_config(opts) do
18+
[
19+
{:name, opts |> Map.fetch!(:database) |> via_tuple()},
20+
{:worker_module, __MODULE__},
21+
{:size, Map.get(opts, :pool_size)},
22+
{:max_overflow, Map.get(opts, :pool_overflow)}
23+
]
24+
end
25+
26+
@doc false
27+
def start_link(opts) do
28+
GenServer.start_link(__MODULE__, opts)
29+
end
30+
31+
@doc """
32+
single pool worker initialization with deferred UDP socket setup
33+
"""
34+
def init(opts) do
35+
Process.send(self(), {:init, opts}, [])
36+
{:ok, %{}}
37+
end
38+
39+
@spec worker_pid(database :: String.t()) :: pid() | :full
40+
@doc """
41+
Perform a non-blocking checkout of an available worker for sending data. If no
42+
worker is available, `:poolboy.checkout/2` will return `:full`
43+
"""
44+
def worker_pid(database) do
45+
:poolboy.checkout(
46+
via_tuple(database),
47+
false
48+
)
49+
end
50+
51+
@spec send_points(pid(), points :: [map() | Inflex.Point.t()]) :: :ok
52+
@doc """
53+
Given an identified worker via `worker_pid/1` and points, asynchronously ship
54+
the points to influx.
55+
"""
56+
def send_points(pid, points), do: GenServer.cast(pid, {:send, points})
57+
58+
@doc false
59+
def handle_cast({:send, points}, state) do
60+
send_batch(points, state)
61+
{:noreply, state}
62+
end
63+
64+
defp send_batch(points, state) do
65+
payload =
66+
points
67+
|> Stream.map(&LineProtocol.encode/1)
68+
|> Enum.join("\n")
69+
70+
UDP.write(
71+
state.udp_socket,
72+
state.config.host,
73+
state.config.udp_port,
74+
payload
75+
)
76+
rescue
77+
e ->
78+
Logger.debug(fn ->
79+
{
80+
Exception.format(:error, e, System.stacktrace()),
81+
[points: inspect(points)]
82+
}
83+
end)
84+
after
85+
# regardless of formatting errors, invalid types, etc, check the worker
86+
# back into the pool
87+
:poolboy.checkin(via_tuple(state.config.database), self())
88+
end
89+
90+
def handle_info({:init, opts}, _state) do
91+
udp_opts =
92+
Map.get(
93+
opts,
94+
:udp_conn_opts
95+
)
96+
97+
socket = UDP.open(0, udp_opts)
98+
99+
config =
100+
opts
101+
|> Map.new()
102+
|> prepare_host()
103+
104+
{:noreply, %{config: config, udp_socket: socket}}
105+
end
106+
107+
defp prepare_host(state),
108+
do: Map.update(state, :host, "", &String.to_charlist/1)
109+
110+
@doc false
111+
def via_tuple(database) do
112+
{:via, Registry, {Inflex.Registry, database <> "_pool"}}
113+
end
114+
115+
def create_point(input) do
116+
%{
117+
measurement: "measure",
118+
fields: %{
119+
input: input,
120+
value: :rand.uniform(10)
121+
},
122+
tags: %{
123+
tag0:
124+
if :rand.uniform() > 0.5 do
125+
"val0"
126+
else
127+
"val1"
128+
end
129+
},
130+
timestamp: System.os_time(:nanosecond)
131+
}
132+
end
133+
end

0 commit comments

Comments
 (0)