From 0cf3212fbe93d4744e5933eb9e250e93a90e1e29 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 07:37:03 -0600 Subject: [PATCH 1/8] added liveview and monitor services --- apps/cronitex/lib/cronitex/application.ex | 5 +- .../cronitex/monitor_services/application.ex | 20 +++++ .../cron_monitors/cron_monitor_server.ex | 77 +++++++++++++++++++ .../cron_monitors/cron_monitor_supervisor.ex | 17 ++++ .../lib/cronitex/monitors/cron_monitor.ex | 2 +- apps/cronitex_web/assets/js/app.js | 20 ++++- apps/cronitex_web/assets/package-lock.json | 3 + apps/cronitex_web/assets/package.json | 3 +- apps/cronitex_web/lib/cronitex_web.ex | 4 +- .../cronmonitor_status_live_view.ex | 21 +++++ apps/cronitex_web/lib/cronitex_web/router.ex | 5 +- .../templates/cron_monitor/index.html.eex | 3 +- .../templates/layout/app.html.eex | 30 +------- .../templates/layout/root.html.eex | 35 +++++++++ apps/cronitex_web/mix.exs | 4 +- mix.lock | 2 + 16 files changed, 211 insertions(+), 40 deletions(-) create mode 100644 apps/cronitex/lib/cronitex/monitor_services/application.ex create mode 100644 apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex create mode 100644 apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex create mode 100644 apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex create mode 100644 apps/cronitex_web/lib/cronitex_web/templates/layout/root.html.eex diff --git a/apps/cronitex/lib/cronitex/application.ex b/apps/cronitex/lib/cronitex/application.ex index 6d8ad5a..b213243 100644 --- a/apps/cronitex/lib/cronitex/application.ex +++ b/apps/cronitex/lib/cronitex/application.ex @@ -10,9 +10,8 @@ defmodule Cronitex.Application do # Start the Ecto repository Cronitex.Repo, # Start the PubSub system - {Phoenix.PubSub, name: Cronitex.PubSub} - # Start a worker by calling: Cronitex.Worker.start_link(arg) - # {Cronitex.Worker, arg} + {Phoenix.PubSub, name: Cronitex.PubSub}, + Cronitex.MonitorServices.CronMonitorSupervisor ] Supervisor.start_link(children, strategy: :one_for_one, name: Cronitex.Supervisor) diff --git a/apps/cronitex/lib/cronitex/monitor_services/application.ex b/apps/cronitex/lib/cronitex/monitor_services/application.ex new file mode 100644 index 0000000..7716b6e --- /dev/null +++ b/apps/cronitex/lib/cronitex/monitor_services/application.ex @@ -0,0 +1,20 @@ +defmodule MonitorService.Application do + # See https://hexdocs.pm/elixir/Application.html + # for more information on OTP Applications + @moduledoc false + + use Application + + def start(_type, _args) do + children = [ + # Starts a worker by calling: MonitorService.Worker.start_link(arg) + # {MonitorService.Worker, arg} + MonitorService.CronMonitorSupervisor + ] + + # See https://hexdocs.pm/elixir/Supervisor.html + # for other strategies and supported options + opts = [strategy: :one_for_one, name: MonitorService.Supervisor] + Supervisor.start_link(children, opts) + end +end diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex new file mode 100644 index 0000000..cd7bde3 --- /dev/null +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex @@ -0,0 +1,77 @@ +defmodule Cronitex.MonitorServices.CronMonitorServer do + use GenServer + alias Phoenix.PubSub + + def start_link(init_arg, options) do + GenServer.start_link(__MODULE__, init_arg, options) + end + + @impl true + def init(state) do + # set the initial state + state = Map.put(state, :monitor_state, :waiting_for_first_ping) + PubSub.broadcast(Cronitex.PubSub, state.config.token, "waiting") + # IO.inspect(state) + + state = schedule_work(state) + # IO.inspect(state) + {:ok, state} + end + + @impl true + def handle_info(:work, state) do + state = schedule_work(state) + state = Map.put(state, :monitor_state, :waiting) + PubSub.broadcast(Cronitex.PubSub, state.config.token, "waiting") + # IO.inspect(state) + {:noreply, state} + end + + @impl true + def handle_info(:start_ping, state) do + # If we get a success ping, disregard the timeout + state = cancel_and_remove_timer(state, :timeout_timer) + state = Map.put(state, :monitor_state, :ok) + PubSub.broadcast(Cronitex.PubSub, state.config.token, "ok") + # IO.inspect(state) + {:noreply, state} + end + + @impl true + def handle_info(:timeout, state) do + state = Map.put(state, :monitor_state, :start_timeout) + PubSub.broadcast(Cronitex.PubSub, state.config.token, "start timeout") + # IO.inspect(state) + {:noreply, state} + end + + @impl true + def handle_info(:stop, state) do + state = cancel_and_remove_timer(state, :work_timer) + state = cancel_and_remove_timer(state, :timeout_timer) + PubSub.broadcast(Cronitex.PubSub, state.config.token, "stopped") + {:noreply, state} + end + + defp cancel_and_remove_timer(state, timer_key) when is_map_key(state, timer_key) do + Process.cancel_timer(state[timer_key]) + Map.pop(state, timer_key) + end + + defp cancel_and_remove_timer(state, _timer_key), do: state + + + defp schedule_work(state) do + # Whenever we schedule work, we need to set two timers, one that executes with the crontab, and one that executes with the timeout + {:ok, next_rundate} = Crontab.Scheduler.get_next_run_date(state.config.cron_expression) + milliseconds_till_run = NaiveDateTime.diff(next_rundate, DateTime.to_naive(DateTime.utc_now()), :millisecond) + timer = Process.send_after(self(), :work, milliseconds_till_run) + state = Map.put(state, :work_timer, timer) + + timeout_milliseconds = state.config.start_tolerance_seconds * 1000 + timeout_timer = Process.send_after(self(), :timeout, milliseconds_till_run + timeout_milliseconds) + state = Map.put(state, :timeout_timer, timeout_timer) + state + end + +end diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex new file mode 100644 index 0000000..8215165 --- /dev/null +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex @@ -0,0 +1,17 @@ +defmodule Cronitex.MonitorServices.CronMonitorSupervisor do + use Supervisor + + alias Cronitex.Monitors + alias Cronitex.MonitorServices.CronMonitorServer + + def start_link(init_arg) do + Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__) + end + + @impl true + def init(_init_arg) do + Monitors.list_cronmonitors() + |> Enum.into([], fn (model) -> %{id: model.token, start: {CronMonitorServer, :start_link, [%{config: model}, []]}} end) + |> Supervisor.init(strategy: :one_for_one) + end +end diff --git a/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex b/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex index d5ac9b9..f890307 100644 --- a/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex +++ b/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex @@ -1,7 +1,6 @@ defmodule Cronitex.Monitors.CronMonitor do use Ecto.Schema import Ecto.Changeset - import Crontab.CronExpression.Ecto.Type schema "cronmonitors" do field :name, :string @@ -21,6 +20,7 @@ defmodule Cronitex.Monitors.CronMonitor do |> validate_required([:name, :cron_expression, :start_tolerance_seconds]) |> unique_constraint(:token) |> unique_constraint([:name, :user_id]) + |> check_cron_expression() |> put_token() end diff --git a/apps/cronitex_web/assets/js/app.js b/apps/cronitex_web/assets/js/app.js index 8c02de5..3ce6849 100644 --- a/apps/cronitex_web/assets/js/app.js +++ b/apps/cronitex_web/assets/js/app.js @@ -12,4 +12,22 @@ import "../css/app.scss" // import {Socket} from "phoenix" // import socket from "./socket" // -import "phoenix_html" \ No newline at end of file +import "phoenix_html" + +// assets/js/app.js +import {Socket} from "phoenix" +import LiveSocket from "phoenix_live_view" + +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) + +// Connect if there are any LiveViews on the page +liveSocket.connect() + +// Expose liveSocket on window for web console debug logs and latency simulation: +// >> liveSocket.enableDebug() +// >> liveSocket.enableLatencySim(1000) +// The latency simulator is enabled for the duration of the browser session. +// Call disableLatencySim() to disable: +// >> liveSocket.disableLatencySim() +window.liveSocket = liveSocket \ No newline at end of file diff --git a/apps/cronitex_web/assets/package-lock.json b/apps/cronitex_web/assets/package-lock.json index f8fc546..6f9b3dc 100644 --- a/apps/cronitex_web/assets/package-lock.json +++ b/apps/cronitex_web/assets/package-lock.json @@ -5264,6 +5264,9 @@ "phoenix_html": { "version": "file:../../../deps/phoenix_html" }, + "phoenix_live_view": { + "version": "file:../../../deps/phoenix_live_view" + }, "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", diff --git a/apps/cronitex_web/assets/package.json b/apps/cronitex_web/assets/package.json index 142edf0..d59c5b3 100644 --- a/apps/cronitex_web/assets/package.json +++ b/apps/cronitex_web/assets/package.json @@ -8,7 +8,8 @@ }, "dependencies": { "phoenix": "file:../../../deps/phoenix", - "phoenix_html": "file:../../../deps/phoenix_html" + "phoenix_html": "file:../../../deps/phoenix_html", + "phoenix_live_view": "file:../../../deps/phoenix_live_view" }, "devDependencies": { "@babel/core": "^7.0.0", diff --git a/apps/cronitex_web/lib/cronitex_web.ex b/apps/cronitex_web/lib/cronitex_web.ex index 6cc590d..8b97080 100644 --- a/apps/cronitex_web/lib/cronitex_web.ex +++ b/apps/cronitex_web/lib/cronitex_web.ex @@ -25,6 +25,7 @@ defmodule CronitexWeb do import CronitexWeb.Gettext import CronitexWeb.Auth, only: [authenticate_user: 2] alias CronitexWeb.Router.Helpers, as: Routes + import Phoenix.LiveView.Controller end end @@ -36,7 +37,7 @@ defmodule CronitexWeb do # Import convenience functions from controllers import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] - + import Phoenix.LiveView.Helpers # Include shared imports and aliases for views unquote(view_helpers()) end @@ -49,6 +50,7 @@ defmodule CronitexWeb do import Plug.Conn import Phoenix.Controller import CronitexWeb.Auth, only: [authenticate_user: 2] + import Phoenix.LiveView.Router end end diff --git a/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex b/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex new file mode 100644 index 0000000..a2a723f --- /dev/null +++ b/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex @@ -0,0 +1,21 @@ +defmodule CronitexWeb.CronMonitorStatusLive do + use Phoenix.LiveView + alias Phoenix.PubSub + + def render(assigns) do + ~L""" + <%= @status %> + """ + end + + def handle_info(message, socket) do + {:noreply, assign(socket, :status, message)} + end + + def mount(_params, %{"monitor_token" => monitor_token}, socket) do + IO.puts("MOUNTING LIVE VIEW to topic #{monitor_token}") + status = PubSub.subscribe(Cronitex.PubSub, monitor_token) + IO.inspect(status) + {:ok, assign(socket, :status, "Ready!")} + end +end diff --git a/apps/cronitex_web/lib/cronitex_web/router.ex b/apps/cronitex_web/lib/cronitex_web/router.ex index 334ca72..ad69671 100644 --- a/apps/cronitex_web/lib/cronitex_web/router.ex +++ b/apps/cronitex_web/lib/cronitex_web/router.ex @@ -4,9 +4,10 @@ defmodule CronitexWeb.Router do pipeline :browser do plug :accepts, ["html"] plug :fetch_session - plug :fetch_flash + plug :fetch_live_flash plug :protect_from_forgery plug :put_secure_browser_headers + plug :put_root_layout, {CronitexWeb.LayoutView, :root} plug CronitexWeb.Auth end @@ -18,7 +19,7 @@ defmodule CronitexWeb.Router do pipe_through :browser get "/", PageController, :index - resources "/users", UserController, only: [:new, :create, :show, :delete] + resources "/users", UserController, only: [:index, :new, :create, :show, :delete, :edit] resources "/sessions", SessionController, only: [:new, :create, :delete] end diff --git a/apps/cronitex_web/lib/cronitex_web/templates/cron_monitor/index.html.eex b/apps/cronitex_web/lib/cronitex_web/templates/cron_monitor/index.html.eex index 82548f7..aeafbf0 100644 --- a/apps/cronitex_web/lib/cronitex_web/templates/cron_monitor/index.html.eex +++ b/apps/cronitex_web/lib/cronitex_web/templates/cron_monitor/index.html.eex @@ -7,7 +7,7 @@ Token Cron expression Start tolerance seconds - + Status @@ -18,6 +18,7 @@ <%= cron_monitor.token %> <%= cron_str(cron_monitor.cron_expression) %> <%= cron_monitor.start_tolerance_seconds %> + <%= live_render(@conn, CronitexWeb.CronMonitorStatusLive, session: %{"monitor_token" => cron_monitor.token}) %> <%= link "Show", to: Routes.cron_monitor_path(@conn, :show, cron_monitor) %> diff --git a/apps/cronitex_web/lib/cronitex_web/templates/layout/app.html.eex b/apps/cronitex_web/lib/cronitex_web/templates/layout/app.html.eex index 47efa44..7282448 100644 --- a/apps/cronitex_web/lib/cronitex_web/templates/layout/app.html.eex +++ b/apps/cronitex_web/lib/cronitex_web/templates/layout/app.html.eex @@ -6,38 +6,10 @@ Cronitex ยท Phoenix Framework "/> + <%= csrf_meta_tag() %> -
-
- - -
-
-
- - <%= @inner_content %> -
diff --git a/apps/cronitex_web/lib/cronitex_web/templates/layout/root.html.eex b/apps/cronitex_web/lib/cronitex_web/templates/layout/root.html.eex new file mode 100644 index 0000000..f28ff68 --- /dev/null +++ b/apps/cronitex_web/lib/cronitex_web/templates/layout/root.html.eex @@ -0,0 +1,35 @@ + + + +
+
+ + +
+
+
+ + + <%= @inner_content %> +
+ + diff --git a/apps/cronitex_web/mix.exs b/apps/cronitex_web/mix.exs index 37fe87c..a69f40a 100644 --- a/apps/cronitex_web/mix.exs +++ b/apps/cronitex_web/mix.exs @@ -48,7 +48,9 @@ defmodule CronitexWeb.MixProject do {:gettext, "~> 0.11"}, {:cronitex, in_umbrella: true}, {:jason, "~> 1.0"}, - {:plug_cowboy, "~> 2.0"} + {:plug_cowboy, "~> 2.0"}, + {:phoenix_live_view, "~> 0.14.8"}, + {:floki, ">= 0.27.0", only: :test} ] end diff --git a/mix.lock b/mix.lock index 6ad5ccb..4c85bfb 100644 --- a/mix.lock +++ b/mix.lock @@ -12,8 +12,10 @@ "ecto_sql": {:hex, :ecto_sql, "3.5.3", "1964df0305538364b97cc4661a2bd2b6c89d803e66e5655e4e55ff1571943efd", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.5.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d2f53592432ce17d3978feb8f43e8dc0705e288b0890caf06d449785f018061c"}, "excoveralls": {:hex, :excoveralls, "0.13.3", "edc5f69218f84c2bf61b3609a22ddf1cec0fbf7d1ba79e59f4c16d42ea4347ed", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "cc26f48d2f68666380b83d8aafda0fffc65dafcc8d8650358e0b61f6a99b1154"}, "file_system": {:hex, :file_system, "0.2.9", "545b9c9d502e8bfa71a5315fac2a923bd060fd9acb797fe6595f54b0f975fd32", [:mix], [], "hexpm", "3cf87a377fe1d93043adeec4889feacf594957226b4f19d5897096d6f61345d8"}, + "floki": {:hex, :floki, "0.29.0", "b1710d8c93a2f860dc2d7adc390dd808dc2fb8f78ee562304457b75f4c640881", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "008585ce64b9f74c07d32958ec9866f4b8a124bf4da1e2941b28e41384edaaad"}, "gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"}, "hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"}, + "html_entities": {:hex, :html_entities, "0.5.1", "1c9715058b42c35a2ab65edc5b36d0ea66dd083767bef6e3edb57870ef556549", [:mix], [], "hexpm", "30efab070904eb897ff05cd52fa61c1025d7f8ef3a9ca250bc4e6513d16c32de"}, "idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"}, "jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, From 9ec4da0b47fa5317bcf7d669cc6dcc06ca41a6fd Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 07:38:06 -0600 Subject: [PATCH 2/8] removed extra file from failed umbrella experiment --- .../cronitex/monitor_services/application.ex | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 apps/cronitex/lib/cronitex/monitor_services/application.ex diff --git a/apps/cronitex/lib/cronitex/monitor_services/application.ex b/apps/cronitex/lib/cronitex/monitor_services/application.ex deleted file mode 100644 index 7716b6e..0000000 --- a/apps/cronitex/lib/cronitex/monitor_services/application.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule MonitorService.Application do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications - @moduledoc false - - use Application - - def start(_type, _args) do - children = [ - # Starts a worker by calling: MonitorService.Worker.start_link(arg) - # {MonitorService.Worker, arg} - MonitorService.CronMonitorSupervisor - ] - - # See https://hexdocs.pm/elixir/Supervisor.html - # for other strategies and supported options - opts = [strategy: :one_for_one, name: MonitorService.Supervisor] - Supervisor.start_link(children, opts) - end -end From b0f74e7257ed93931e6c77b2b0ab0ae70c8a873b Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 08:48:52 -0600 Subject: [PATCH 3/8] add module to simplify live updates --- .../cron_monitors/cron_monitor_server.ex | 28 ++++++++----------- .../cronitex/monitor_services/live_updates.ex | 11 ++++++++ .../cronmonitor_status_live_view.ex | 8 +++--- 3 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 apps/cronitex/lib/cronitex/monitor_services/live_updates.ex diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex index cd7bde3..4666b38 100644 --- a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex @@ -1,6 +1,7 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do use GenServer alias Phoenix.PubSub + alias Cronitex.MonitorServices.LiveUpdates def start_link(init_arg, options) do GenServer.start_link(__MODULE__, init_arg, options) @@ -8,22 +9,15 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do @impl true def init(state) do - # set the initial state - state = Map.put(state, :monitor_state, :waiting_for_first_ping) - PubSub.broadcast(Cronitex.PubSub, state.config.token, "waiting") - # IO.inspect(state) - + state = update_monitor_state(state, :waiting) state = schedule_work(state) - # IO.inspect(state) {:ok, state} end @impl true def handle_info(:work, state) do state = schedule_work(state) - state = Map.put(state, :monitor_state, :waiting) - PubSub.broadcast(Cronitex.PubSub, state.config.token, "waiting") - # IO.inspect(state) + state = update_monitor_state(state, :waiting) {:noreply, state} end @@ -31,17 +25,13 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do def handle_info(:start_ping, state) do # If we get a success ping, disregard the timeout state = cancel_and_remove_timer(state, :timeout_timer) - state = Map.put(state, :monitor_state, :ok) - PubSub.broadcast(Cronitex.PubSub, state.config.token, "ok") - # IO.inspect(state) + state = update_monitor_state(state, :ok) {:noreply, state} end @impl true def handle_info(:timeout, state) do - state = Map.put(state, :monitor_state, :start_timeout) - PubSub.broadcast(Cronitex.PubSub, state.config.token, "start timeout") - # IO.inspect(state) + state = update_monitor_state(state, :start_timeout) {:noreply, state} end @@ -49,7 +39,7 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do def handle_info(:stop, state) do state = cancel_and_remove_timer(state, :work_timer) state = cancel_and_remove_timer(state, :timeout_timer) - PubSub.broadcast(Cronitex.PubSub, state.config.token, "stopped") + state = update_monitor_state(state, :stopped) {:noreply, state} end @@ -60,6 +50,12 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do defp cancel_and_remove_timer(state, _timer_key), do: state + defp update_monitor_state(state, monitor_state) do + state = Map.put(state, :monitor_state, monitor_state) + LiveUpdates.notify_live_view_for_monitor_id(state.config.token, monitor_state) + state + end + defp schedule_work(state) do # Whenever we schedule work, we need to set two timers, one that executes with the crontab, and one that executes with the timeout diff --git a/apps/cronitex/lib/cronitex/monitor_services/live_updates.ex b/apps/cronitex/lib/cronitex/monitor_services/live_updates.ex new file mode 100644 index 0000000..84e442c --- /dev/null +++ b/apps/cronitex/lib/cronitex/monitor_services/live_updates.ex @@ -0,0 +1,11 @@ +defmodule Cronitex.MonitorServices.LiveUpdates do + alias Phoenix + + def subscribe_live_view_for_monitor_id(monitor_id) do + Phoenix.PubSub.subscribe(Cronitex.PubSub, monitor_id, link: true) + end + + def notify_live_view_for_monitor_id(monitor_id, status) do + Phoenix.PubSub.broadcast(Cronitex.PubSub, monitor_id, status) + end +end diff --git a/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex b/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex index a2a723f..e965d50 100644 --- a/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex +++ b/apps/cronitex_web/lib/cronitex_web/live_views/cronmonitor_status_live_view.ex @@ -1,6 +1,7 @@ defmodule CronitexWeb.CronMonitorStatusLive do use Phoenix.LiveView - alias Phoenix.PubSub + alias Cronitex.MonitorServices.LiveUpdates + def render(assigns) do ~L""" @@ -13,9 +14,8 @@ defmodule CronitexWeb.CronMonitorStatusLive do end def mount(_params, %{"monitor_token" => monitor_token}, socket) do - IO.puts("MOUNTING LIVE VIEW to topic #{monitor_token}") - status = PubSub.subscribe(Cronitex.PubSub, monitor_token) - IO.inspect(status) + LiveUpdates.subscribe_live_view_for_monitor_id(monitor_token) + {:ok, assign(socket, :status, "Ready!")} end end From 74c0218007b2705d2368d48feb51c82d6d2ec4ff Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 09:03:46 -0600 Subject: [PATCH 4/8] validation doesn't need to happen in the changeset, since the cast to the correct cron expression type handles validation --- apps/cronitex/lib/cronitex/monitors/cron_monitor.ex | 9 --------- 1 file changed, 9 deletions(-) diff --git a/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex b/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex index f890307..cabaa10 100644 --- a/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex +++ b/apps/cronitex/lib/cronitex/monitors/cron_monitor.ex @@ -20,18 +20,9 @@ defmodule Cronitex.Monitors.CronMonitor do |> validate_required([:name, :cron_expression, :start_tolerance_seconds]) |> unique_constraint(:token) |> unique_constraint([:name, :user_id]) - |> check_cron_expression() |> put_token() end - defp check_cron_expression(changeset) do - case Crontab.CronExpression.Parser.parse(changeset.data.cron_expression) do - {:ok, _expression} -> changeset - {:error, _error} -> add_error(changeset, :cron_expression, "Invalid Cron Expression.") - end - - end - defp put_token(changeset) do unless changeset.data.token do put_change(changeset, :token, Ecto.UUID.generate()) From 9e0ce3c8aaaef73d8ada1c8871d0f41b827672f3 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 09:39:16 -0600 Subject: [PATCH 5/8] update entrypoint to do npm install after volume is mounted --- entrypoint.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 99ff3d7..07291cb 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -18,6 +18,10 @@ if [[ -z `psql -Atqc "\\list $PGDATABASE"` ]]; then echo "Database $PGDATABASE created." fi + +cd /app/apps/cronitex_web/assets && npm install +cd /app + # exec iex --cookie secret --name cronitex@docker \ # --erl '-kernel inet_dist_listen_min 9000' \ # --erl '-kernel inet_dist_listen_max 9000' \ From 68bc3a7ae9338edf7ba867c668a0fca53d49e2e8 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 5 Nov 2020 15:39:49 -0600 Subject: [PATCH 6/8] add test for live updates code --- .../cron_monitors/cron_monitor_supervisor.ex | 14 ++++++++++- .../monitor_services/live_updates_test.exs | 25 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex index 8215165..fa90fc5 100644 --- a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex @@ -11,7 +11,19 @@ defmodule Cronitex.MonitorServices.CronMonitorSupervisor do @impl true def init(_init_arg) do Monitors.list_cronmonitors() - |> Enum.into([], fn (model) -> %{id: model.token, start: {CronMonitorServer, :start_link, [%{config: model}, []]}} end) + |> start_cron_monitor_server() + end + + def start_cron_monitor_server(cron_monitor_model) do + cron_monitor_model + |> Enum.into([], &cron_monitor_to_child_map/1) |> Supervisor.init(strategy: :one_for_one) end + + defp cron_monitor_to_child_map(model) do + %{ + id: model.token, + start: {CronMonitorServer, :start_link, [%{config: model}, []]} + } + end end diff --git a/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs b/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs new file mode 100644 index 0000000..c2b01be --- /dev/null +++ b/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs @@ -0,0 +1,25 @@ +defmodule Cronitex.MonitorServices.LiveUpdatesTest do + use Cronitex.DataCase, async: true + + alias Cronitex.MonitorServices.LiveUpdates + + @monitor_id "1234-1234-1234-1234" + + test "nofify and subscribe for monitor token work correctly" do + # Subscribe to notifications + LiveUpdates.subscribe_live_view_for_monitor_id(@monitor_id) + + # Make sure we have no messages + {:messages, messages} = Process.info(self(), :messages) + assert Enum.count(messages) == 0 + + # Notifiy on the same topic, and assert that we have one message with our payload + LiveUpdates.notify_live_view_for_monitor_id(@monitor_id, :hello) + {:messages, messages} = Process.info(self(), :messages) + + assert Enum.count(messages) == 1 + [message | _tail] = messages + assert message == :hello + end + + end From 07e6ccb5fa8238878d4e015ffbd63a40e9ed3763 Mon Sep 17 00:00:00 2001 From: Andy Date: Sat, 7 Nov 2020 22:36:59 -0600 Subject: [PATCH 7/8] finally got a test working for the supervisor to create children --- .../cron_monitors/cron_monitor_server.ex | 1 - .../cron_monitors/cron_monitor_supervisor.ex | 6 +-- .../cron_monitor_supervisor_test.exs | 37 +++++++++++++++++++ .../monitor_services/live_updates_test.exs | 2 +- apps/cronitex/test/cronitex/monitors_test.exs | 2 +- apps/cronitex/test/support/data_case.ex | 10 +++-- 6 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 apps/cronitex/test/cronitex/monitor_services/cron_monitor_supervisor_test.exs diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex index 4666b38..e3042ab 100644 --- a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_server.ex @@ -1,6 +1,5 @@ defmodule Cronitex.MonitorServices.CronMonitorServer do use GenServer - alias Phoenix.PubSub alias Cronitex.MonitorServices.LiveUpdates def start_link(init_arg, options) do diff --git a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex index fa90fc5..8a38faa 100644 --- a/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex +++ b/apps/cronitex/lib/cronitex/monitor_services/cron_monitors/cron_monitor_supervisor.ex @@ -11,11 +11,11 @@ defmodule Cronitex.MonitorServices.CronMonitorSupervisor do @impl true def init(_init_arg) do Monitors.list_cronmonitors() - |> start_cron_monitor_server() + |> start_cron_monitor_servers() end - def start_cron_monitor_server(cron_monitor_model) do - cron_monitor_model + def start_cron_monitor_servers(monitors) do + monitors |> Enum.into([], &cron_monitor_to_child_map/1) |> Supervisor.init(strategy: :one_for_one) end diff --git a/apps/cronitex/test/cronitex/monitor_services/cron_monitor_supervisor_test.exs b/apps/cronitex/test/cronitex/monitor_services/cron_monitor_supervisor_test.exs new file mode 100644 index 0000000..5658311 --- /dev/null +++ b/apps/cronitex/test/cronitex/monitor_services/cron_monitor_supervisor_test.exs @@ -0,0 +1,37 @@ +defmodule Cronitex.MonitorServices.CronMonitorSupervisorTests do + use Cronitex.DataCase + + alias Cronitex.MonitorServices.CronMonitorSupervisor + alias Cronitex.TestHelpers + alias Cronitex.Monitors + + test "supervisor spawns with correct children" do + pid = Process.whereis(CronMonitorSupervisor) + %{active: active} = Supervisor.count_children(pid) + assert active == 0 + end + + test "supervisor respawns with corrct children" do + user = TestHelpers.user_fixture() + {:ok, monitor} = Monitors.create_cron_monitor(user, %{name: "valid cronmon", cron_expression: "* * * * * *"}) + + pid = Process.whereis(CronMonitorSupervisor) + ref = Process.monitor(pid) + Process.exit(pid, :kill) + receive do + {:DOWN, ^ref, :process, ^pid, :killed} -> + :timer.sleep 1 + + # Now that we've created a monitor and restarted the process, we should expect that there's an active child of + # our supervisor + pid = Process.whereis(CronMonitorSupervisor) + %{active: active} = Supervisor.count_children(pid) + assert active == 1 + after + 1000 -> + raise :timeout + end + + end + +end diff --git a/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs b/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs index c2b01be..f6822c0 100644 --- a/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs +++ b/apps/cronitex/test/cronitex/monitor_services/live_updates_test.exs @@ -22,4 +22,4 @@ defmodule Cronitex.MonitorServices.LiveUpdatesTest do assert message == :hello end - end +end diff --git a/apps/cronitex/test/cronitex/monitors_test.exs b/apps/cronitex/test/cronitex/monitors_test.exs index e33aaf5..7d9c203 100644 --- a/apps/cronitex/test/cronitex/monitors_test.exs +++ b/apps/cronitex/test/cronitex/monitors_test.exs @@ -1,5 +1,5 @@ defmodule Cronitex.MonitorsTest do - use Cronitex.DataCase + use Cronitex.DataCase, async: true import Crontab.CronExpression alias Cronitex.Monitors diff --git a/apps/cronitex/test/support/data_case.ex b/apps/cronitex/test/support/data_case.ex index ce39b2b..3c6d6f0 100644 --- a/apps/cronitex/test/support/data_case.ex +++ b/apps/cronitex/test/support/data_case.ex @@ -27,10 +27,14 @@ defmodule Cronitex.DataCase do end end - setup _tags do - Ecto.Adapters.SQL.Sandbox.checkout(Cronitex.Repo) + setup tags do + :ok = Ecto.Adapters.SQL.Sandbox.checkout(Cronitex.Repo) - # Don't need handling for other tags here, will only be using postgres. + unless tags[:async] do + Ecto.Adapters.SQL.Sandbox.mode(Cronitex.Repo, {:shared, self()}) + end + + :ok end @doc """ From 53957ccac24b0e02cc3839fd93d7947b8f14e1d7 Mon Sep 17 00:00:00 2001 From: Andy Date: Sun, 22 Nov 2020 14:07:07 -0600 Subject: [PATCH 8/8] remove travis file --- .travis.yml | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b9f5420..0000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: elixir - -services: - - docker - -before_script: - - docker-compose build - - docker-compose up -d - - sleep 10 - -script: - - docker-compose exec cronitex sh -c 'MIX_ENV=test mix do compile --umbrella, coveralls.json' - -after_success: - - bash <(curl -s https://codecov.io/bash) \ No newline at end of file