chore: update dependencies. phoenix 1.8.8→1.8.9, swoosh 1.26.2→1.26.3, oban_pro 1.7.6→1.7.7, oban_web 2.12.5→2.12.6, hackney 4.4.5→4.5.2, plus transitive deps

This commit is contained in:
Graham McIntire 2026-07-08 10:47:26 -05:00
parent bdb0dd8c38
commit a719135c1e
31 changed files with 402 additions and 431 deletions

View file

@ -11,11 +11,11 @@
"cloak": {:hex, :cloak, "1.1.4", "aba387b22ea4d80d92d38ab1890cc528b06e0e7ef2a4581d71c3fdad59e997e7", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "92b20527b9aba3d939fab0dd32ce592ff86361547cfdc87d74edce6f980eb3d7"},
"cloak_ecto": {:hex, :cloak_ecto, "1.3.0", "0de127c857d7452ba3c3367f53fb814b0410ff9c680a8d20fbe8b9a3c57a1118", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "314beb0c123b8a800418ca1d51065b27ba3b15f085977e65c0f7b2adab2de1cc"},
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
"cowboy": {:hex, :cowboy, "2.16.1", "fa04080b602ff25c40a7700f2dc0152dbc1ba26b42093ae0fa9bb7a337d5a242", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "b8ea4dd317a043e3177ec840cfa3bcb47cfb41035d3abb24d954dc7d51def399"},
"cowboy": {:hex, :cowboy, "2.17.0", "059d6ae769214cedf55d115ce5bf38649ff6b32ec693067cf6b185d0ab1b53c3", [:make, :rebar3], [{:cowlib, ">= 2.18.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "84f0e4c9d5820342cd506472052b79336d0d9d3624e46303f6d0af9dc94e8d5f"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.17.1", "3e6053016d1ab245730f0af688755476dcedb1c25ed8fb5751f59a2bfdc0c9af", [:make, :rebar3], [], "hexpm", "ff08bd17e6dd931445b18af77315b9b5fe052407110964ad2588c686b57b5e3f"},
"cowlib": {:hex, :cowlib, "2.18.0", "4a3ef8cb012c21c7cbb7c925f75405e26d7fcd393fd78c46187539dbc9a48310", [:make, :rebar3], [], "hexpm", "c4f89d33a61162d4cfdcb5a1af7e562d80a700b2573cc71020ce6521b152dc1a"},
"credo": {:hex, :credo, "1.7.19", "cc52129665fc7c15143d47838fda0f9cd6dac9ceced7bf4da6f85fcbfe64b12a", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2d8bc95d5a7bb99dd2613621d4f08c6a3575c3fd4b62e6a2b48a100352a557b8"},
"db_connection": {:hex, :db_connection, "2.10.1", "d5465f6bcc125c1b8981c1dbf23c193ca16f446ec0b25832dc174f74f18be510", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "18ed94c6e627b4bf452dbd4df61b69a35a1e768525140bc1917b7a685026a6a3"},
"db_connection": {:hex, :db_connection, "2.10.2", "ae391e803a5adff104da913c2fc1c0c14a37f8b10001dcef568796e1fb7bf95c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "510b14482330f1af6490a2fa0efd8d4f1435d1529b165647df22ac0f2df0fa93"},
"decimal": {:hex, :decimal, "3.1.1", "430d87b04011ce6cbd4fd205be758311a81f87d552d40904abd00f015935b1d0", [:mix], [], "hexpm", "c5f25f2ced74a0587d03e6023f595db8e924c9d3922c8c8ffd9edfc4498cf1f6"},
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
@ -36,10 +36,10 @@
"geo": {:hex, :geo, "4.1.0", "64ba89a64cc400b5b16dd2f5bd644cb141776eb8c2ac5a983332c8d944936c12", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "19edb2b3398ca9f701b573b1fb11bc90951ebd64f18b06bd1bf35abe509a2934"},
"geo_postgis": {:hex, :geo_postgis, "3.7.1", "614f25b42334a615bd54bb09c22030b1aac7bac8f829bd823ab1faccf093a324", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:geo, "~> 3.6 or ~> 4.0", [hex: :geo, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0 or ~> 4.0 or ~> 5.0 or ~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "c20d823c600d35b7fe9ddd5be03052bb7136c57d6f1775dbd46871545e405280"},
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
"h2": {:hex, :h2, "0.10.2", "ea0146b9c8b5f3b5de16045765f5684db38ef1e66f1c60444890948cb1003e47", [:rebar3], [], "hexpm", "497a899f338b42e6a0b292524e635b0ce6f9379fa39395c8e38d06351cd9b9cf"},
"hackney": {:hex, :hackney, "4.4.5", "a908f620525bb886a16613532324762e5166287f8c00c9888a762edee11a30c0", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.10.1", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 1.6.5", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.1", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "6d72bef4e135e94c522c271e11fbb6933efb0006ef235a3933807d0be73b71ec"},
"h2": {:hex, :h2, "0.10.4", "31ef580056e19977b6542eea2718aa19a45841205cd82cd48535dd7683caea4c", [:rebar3], [], "hexpm", "8350920ac7c259fb759a2830d3ce0dd248c48422a031d1760bebdd2020fa30a9"},
"hackney": {:hex, :hackney, "4.5.2", "d44b00b6e91ef8c480ec51ad42e9d8777a3a391ea78865cb16c5c0645399c6f5", [:rebar3], [{:certifi, "~> 2.17.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:h2, "~> 0.10.4", [hex: :h2, repo: "hexpm", optional: false]}, {:idna, "~> 7.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.2", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:quic, "~> 1.7.0", [hex: :quic, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:webtransport, "~> 0.4.3", [hex: :webtransport, repo: "hexpm", optional: false]}], "hexpm", "5db2e2d7de05cafcd681e21120ae4c8b5a378ebeb9b8d8835f26f456a9a80f7a"},
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"hpax": {:hex, :hpax, "1.0.4", "777de5d433b0fbdc7c418159c8055910faa8047ffdb3d6b31098d2a46cd7685c", [:mix], [], "hexpm", "afc7cb142ebcc2d01ce7816190b98ce5dd49e799111b24249f3443d730f377ca"},
"httpoison": {:hex, :httpoison, "3.0.0", "8566a933bb9175236d1ec335978445b67cd1f5b5d3ead6ca4b80be469d41f5d9", [:mix], [{:hackney, "~> 4.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "9130197b7658901c493d6fcfb842fb9676300fa8a6c8ed058c8889cf1a77f3c2"},
"idna": {:hex, :idna, "7.1.0", "1067a13043538129602d2f2ce6899d8713125c7d19734aa557ce2e3ea55bd4f1", [:rebar3], [], "hexpm", "6ae959a025bf36df61a8cab8508d9654891b5426a84c44d82deaffd6ddf8c71f"},
"inet_cidr": {:hex, :inet_cidr, "1.0.9", "e0ef72a2942529da78c8e4147d53f2ef5f6f5293335c3637b0fdf83c012cc816", [:mix], [], "hexpm", "172da15ff7cf635b1feaf14f5818be28c811b37cc5fb7c5f7c01058c1c1066cc"},
@ -51,7 +51,7 @@
"logger_file_backend": {:hex, :logger_file_backend, "0.0.14", "774bb661f1c3fed51b624d2859180c01e386eb1273dc22de4f4a155ef749a602", [:mix], [], "hexpm", "071354a18196468f3904ef09413af20971d55164267427f6257b52cfba03f9e6"},
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
"mimerl": {:hex, :mimerl, "1.5.0", "f35aca6f23242339b3666e0ac0702379e362b469d0aea167f6cc713547e777ed", [:rebar3], [], "hexpm", "db648ce065bae14ea84ca8b5dd123f42f49417cef693541110bf6f9e9be9ecc4"},
"mint": {:hex, :mint, "1.9.0", "d6f534c2a3e98b2a8cc749b4796eb77e9e3af79a76f96e4c74035a827de0d318", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "007154c7d8c43916aed3c93afd1f11aebbaa9c5ff4b7ba55ebe0d17ee0296042"},
"mint": {:hex, :mint, "1.9.1", "3bc120b743ed2e99ad920910f2613e9faebabb2257731b0e2ea4d8ccd9eceede", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "831101bd560b086316fab5f7adb21a4f3455717d8e4bc8368b052e09aa9163e0"},
"mix_audit": {:hex, :mix_audit, "2.1.5", "c0f77cee6b4ef9d97e37772359a187a166c7a1e0e08b50edf5bf6959dfe5a016", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "87f9298e21da32f697af535475860dc1d3617a010e0b418d2ec6142bc8b42d69"},
"mix_test_watch": {:hex, :mix_test_watch, "1.4.0", "d88bcc4fbe3198871266e9d2f00cd8ae350938efbb11d3fa1da091586345adbb", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2b4693e17c8ead2ef56d4f48a0329891e8c2d0d73752c0f09272a2b17dc38d1b"},
"mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"},
@ -67,7 +67,7 @@
"octo_fetch": {:hex, :octo_fetch, "0.5.0", "f50701568b9fc752656367f82cc134d5fbefff37c5a0e8ddfcceb02ceee3f5fc", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "6226cc3c14ca948ee9f25fb0446322e5c288e215da9beba7899b6b5f4cd3ccb0"},
"parse_trans": {:hex, :parse_trans, "3.4.2", "c352ddc1a0d5e54f9b1654d45f9c432eef76f9cea371c55ddff769ef688fdb74", [:rebar3], [], "hexpm", "4c25347de3b7c35732d32e69ab43d1ceee0beae3f3b3ade1b59cbd3dd224d9ca"},
"peep": {:hex, :peep, "4.4.0", "4b6289e7258ecf2741041068932455eb8389673bca785623621ddb6935286845", [:mix], [{:nimble_options, "~> 1.1", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:plug, "~> 1.16", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f289959a9e5fcf92f5a4becaf4dd0df95c58841368f6ad0574b8ea830a7c1453"},
"phoenix": {:hex, :phoenix, "1.8.8", "ada3d761359274178180c0e992ef0c2b536bd7c3bd75ebba94acbf39ab4347fe", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "f0c843037bd2e7012fc1d1ec9574dfa6972b7e3d09e9b77fd23aa283af0aa994"},
"phoenix": {:hex, :phoenix, "1.8.9", "a63ed0962ed5b903b146dab0ae8eb8387fe478f8171a5e26d56a165f35996fe1", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "3477e2dd5a4f61820341169031bdfe21275f659923bea9c5c0ea2aa1c3fcc046"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
@ -82,7 +82,7 @@
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.22.2", "4aec14df2a72722aee92492566edbeeb44e233ecb86b1915d03136297ef1385d", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "8946382ddb06294f56026ac4278b3cc212bac8a2c82ed68b4087819ed1abc53b"},
"prom_ex": {:hex, :prom_ex, "1.12.0", "a82cbd5b49964e4d4295ab72a18e007aadd5f4a91630b7c49fad42cc7e49c880", [:mix], [{:absinthe, ">= 1.8.0", [hex: :absinthe, repo: "hexpm", optional: true]}, {:broadway, ">= 1.1.0", [hex: :broadway, repo: "hexpm", optional: true]}, {:ecto, ">= 3.14.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:finch, "~> 0.18", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, ">= 2.10.0", [hex: :oban, repo: "hexpm", optional: true]}, {:octo_fetch, "~> 0.4", [hex: :octo_fetch, repo: "hexpm", optional: false]}, {:peep, "~> 3.0 or ~> 4.0", [hex: :peep, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.7.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, ">= 0.20.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, ">= 1.16.0", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 2.6.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, ">= 1.0.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.2", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.1", [hex: :telemetry_poller, repo: "hexpm", optional: false]}], "hexpm", "6357484941489ba2fee64bb1e9f2a1e86309545304f9e90144e3c10e553c958b"},
"quic": {:hex, :quic, "1.6.5", "28b7d49c1732b2de3861701bb03ae7798537240552370ac49f0d139d2ea8f621", [:rebar3], [], "hexpm", "de1a88972c33201a50d1a17c8c4a14528bd1d8f25ef705897d680ae312d0aa78"},
"quic": {:hex, :quic, "1.7.0", "ebcf105fb16ca4d954c3bbd2e5bcda7650074f3e3a9ca80135dbee2607e0c27e", [:rebar3], [], "hexpm", "d0d304e5fda48754a570b90b103153f780f9926c15f6eabfe1b343e4f61e32df"},
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
"redix": {:hex, :redix, "1.5.3", "4eaae29c75e3285c0ff9957046b7c209aa7f72a023a17f0a9ea51c2a50ab5b0f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:nimble_options, "~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7b06fb5246373af41f5826b03334dfa3f636347d4d5d98b4d455b699d425ae7e"},
"req": {:hex, :req, "0.6.2", "b9b2024f35bcf60a92cc8cad2eaaf9d4e7aace463ff74be1afe5986830184413", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.21", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cc9cd30a2ddd04989929b887178e1610c940456d962c6c3a52df6146d2eef9bf"},
@ -90,7 +90,7 @@
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
"styler": {:hex, :styler, "1.11.0", "35010d970689a23c2bcc8e97bd8bf7d20e3561d60c49be84654df5c37d051a9c", [:mix], [], "hexpm", "70f36165d0cf238a32b7a456fdef6a9c72e77e657d7ac4a0ace33aeba3f2b8c0"},
"sweet_xml": {:hex, :sweet_xml, "0.7.5", "803a563113981aaac202a1dbd39771562d0ad31004ddbfc9b5090bdcd5605277", [:mix], [], "hexpm", "193b28a9b12891cae351d81a0cead165ffe67df1b73fe5866d10629f4faefb12"},
"swoosh": {:hex, :swoosh, "1.26.2", "1e5f398c6ab313d028b9bf604165f051e279093090c50b6f93090cf6a599b0f7", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "08c6a1636b82721d0f64259053a733526526d077011ff6a7776f80e21dc60757"},
"swoosh": {:hex, :swoosh, "1.26.3", "9d8b60077305ce259298d9a1102e5be67cd3c41d1ea930c29e9288af195ca017", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, ">= 1.9.0 and < 5.0.0", [hex: :hackney, repo: "hexpm", optional: true]}, {:idna, ">= 6.0.0 and < 8.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c7683d070fe8f8aa9d174e61b01f2d527be73cd8ac40037b7109184941eb569f"},
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
"tailwind": {:hex, :tailwind, "0.5.1", "35435b13158c90d37da11e1cfc808755fca1d7b6c5ab87b1b19c5de87e2f0a10", [:mix], [], "hexpm", "c4e26302a59fec72abc5610ecb6ad2116d9aa31f31aab2d4b8eb6e95d25a689c"},
"telemetry": {:hex, :telemetry, "1.4.2", "a0cb522801dffb1c49fe6e30561badffc7b6d0e180db1300df759faa22062855", [:rebar3], [], "hexpm", "928f6495066506077862c0d1646609eed891a4326bee3126ba54b60af61febb1"},
@ -100,8 +100,8 @@
"thousand_island": {:hex, :thousand_island, "1.5.0", "f50a213cac97262b6d5ebb85745aa2c00fec1413191e6e66834788d45425cecb", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "708923d40523e43cf99041ab37a0d4b0ec426ac6438fa3716ab23d919eaeb412"},
"tzdata": {:hex, :tzdata, "1.1.4", "989e0855df5ee8c40eb929ad971bedd7a51e2c223f051fd95cc2d2cf56a60c19", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "ab48888699de8ff4a255522fd858abe81bac2e64690a375e6cb590112cf4a24e"},
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
"webtransport": {:hex, :webtransport, "0.4.1", "60ef6cd99282b5964c8172d674519239e31e357d934bde028bec70da34636fe5", [:rebar3], [{:h2, "~> 0.10.1", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.6.5", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "006e4e52a8f03b69201d4637c85b424a4ddccc1909f32c5742fed8d495a75174"},
"websock_adapter": {:hex, :websock_adapter, "0.6.0", "73db5ab8aaefd1a876a97ce3e6afc96562625de69ef17a4e04426e034849d0b8", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "50021a85bce8f203b086705d9e0c5415e2c7eb05d319111b0428fe71f9934617"},
"webtransport": {:hex, :webtransport, "0.4.3", "df0c53da138cdc5f3390a31ef612caa3a041c23e18c1a0b6fbd4d72743277806", [:rebar3], [{:h2, "~> 0.10.4", [hex: :h2, repo: "hexpm", optional: false]}, {:quic, "~> 1.7.0", [hex: :quic, repo: "hexpm", optional: false]}], "hexpm", "3ae8c76696cdb56bee3caf1dd853a0a596339886468ca17c578a9bfd647dab9d"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.12.2", "9dd1330fb4cd9a36a7b0f502e5b12486eff632792ee4a5f0eba52a4d4ec32c9c", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "e7c1b10122f973e6558462d51c39026ba0e14afbc6745318e990ea82cfe9e159"},
}

BIN
vendor/oban_pro/.hex vendored

Binary file not shown.

View file

@ -1,80 +0,0 @@
{<<"links">>,[]}.
{<<"name">>,<<"oban_pro">>}.
{<<"version">>,<<"1.7.6">>}.
{<<"description">>,<<"Oban Pro Component">>}.
{<<"elixir">>,<<"~> 1.15">>}.
{<<"files">>,
[<<"lib/oban">>,<<"lib/oban/pro">>,<<"lib/oban/pro/diagnostics.ex">>,
<<"lib/oban/pro/cron.ex">>,<<"lib/oban/pro/decorator.ex">>,
<<"lib/oban/pro/migrations">>,<<"lib/oban/pro/migrations/v1_4_0.ex">>,
<<"lib/oban/pro/migrations/v1_5_0.ex">>,
<<"lib/oban/pro/migrations/v1_6_0.ex">>,
<<"lib/oban/pro/migrations/v1_7_0.ex">>,
<<"lib/oban/pro/migrations/dynamic_partitioner.ex">>,
<<"lib/oban/pro/migrations/v1_0_0.ex">>,<<"lib/oban/pro/unique.ex">>,
<<"lib/oban/pro/batch.ex">>,<<"lib/oban/pro/chunk.ex">>,
<<"lib/oban/pro/producer.ex">>,<<"lib/oban/pro/worker.ex">>,
<<"lib/oban/pro/stages">>,<<"lib/oban/pro/stages/rate.ex">>,
<<"lib/oban/pro/stages/chain.ex">>,<<"lib/oban/pro/stages/encrypted.ex">>,
<<"lib/oban/pro/stages/recorded.ex">>,<<"lib/oban/pro/stages/hooks.ex">>,
<<"lib/oban/pro/stages/deadline.ex">>,
<<"lib/oban/pro/stages/executing.ex">>,
<<"lib/oban/pro/stages/structured.ex">>,<<"lib/oban/pro/plugins">>,
<<"lib/oban/pro/plugins/dynamic_prioritizer.ex">>,
<<"lib/oban/pro/plugins/dynamic_pruner.ex">>,
<<"lib/oban/pro/plugins/dynamic_scaler.ex">>,
<<"lib/oban/pro/plugins/dynamic_partitioner.ex">>,
<<"lib/oban/pro/plugins/dynamic_lifeline.ex">>,
<<"lib/oban/pro/plugins/dynamic_queues.ex">>,
<<"lib/oban/pro/plugins/dynamic_cron.ex">>,<<"lib/oban/pro/limiters">>,
<<"lib/oban/pro/limiters/local.ex">>,<<"lib/oban/pro/limiters/rate.ex">>,
<<"lib/oban/pro/limiters/rate">>,
<<"lib/oban/pro/limiters/rate/token_bucket.ex">>,
<<"lib/oban/pro/limiters/rate/algorithm.ex">>,
<<"lib/oban/pro/limiters/rate/fixed_window.ex">>,
<<"lib/oban/pro/limiters/rate/sliding_window.ex">>,
<<"lib/oban/pro/limiters/global.ex">>,<<"lib/oban/pro/queue.ex">>,
<<"lib/oban/pro/handler.ex">>,<<"lib/oban/pro/partition.ex">>,
<<"lib/oban/pro/cloud.ex">>,<<"lib/oban/pro/uuidv7.ex">>,
<<"lib/oban/pro/stage.ex">>,<<"lib/oban/pro/refresher.ex">>,
<<"lib/oban/pro/workflow.ex">>,<<"lib/oban/pro/testing.ex">>,
<<"lib/oban/pro/migration.ex">>,<<"lib/oban/pro/engines">>,
<<"lib/oban/pro/engines/smart.ex">>,<<"lib/oban/pro/workflow">>,
<<"lib/oban/pro/workflow/schema.ex">>,
<<"lib/oban/pro/workflow/cascade.ex">>,<<"lib/oban/pro/limiter.ex">>,
<<"lib/oban/pro/validation.ex">>,<<"lib/oban/pro/flusher.ex">>,
<<"lib/oban/pro/workers">>,<<"lib/oban/pro/workers/chain.ex">>,
<<"lib/oban/pro/workers/batch.ex">>,<<"lib/oban/pro/workers/chunk.ex">>,
<<"lib/oban/pro/workers/workflow.ex">>,<<"lib/oban/pro/application.ex">>,
<<"lib/oban/pro/utils.ex">>,<<"lib/oban/pro/relay.ex">>,
<<"lib/oban/pro/exceptions.ex">>,<<"lib/oban/pro/rate_limit.ex">>,
<<"lib/oban/pro.ex">>,<<"usage-rules">>,<<"usage-rules/workflows.md">>,
<<"usage-rules/chunks.md">>,<<"usage-rules/workers.md">>,
<<"usage-rules/testing.md">>,<<"usage-rules/batches.md">>,
<<"usage-rules/queues.md">>,<<"usage-rules/plugins.md">>,
<<"usage-rules/chains.md">>,<<".formatter.exs">>,<<"mix.exs">>,
<<"usage-rules.md">>]}.
{<<"app">>,<<"oban_pro">>}.
{<<"licenses">>,[<<"Commercial">>]}.
{<<"requirements">>,
[[{<<"name">>,<<"oban">>},
{<<"app">>,<<"oban">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 2.21">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"ecto_sql">>},
{<<"app">>,<<"ecto_sql">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 3.12">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"postgrex">>},
{<<"app">>,<<"postgrex">>},
{<<"optional">>,true},
{<<"requirement">>,<<"~> 0.22">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"decimal">>},
{<<"app">>,<<"decimal">>},
{<<"optional">>,true},
{<<"requirement">>,<<"~> 3.0">>},
{<<"repository">>,<<"hexpm">>}]]}.
{<<"build_tools">>,[<<"mix">>]}.

View file

@ -17,7 +17,11 @@ defmodule Oban.Pro.Application do
def start(_type, _args) do
for handler <- @handlers, do: handler.on_start()
children = [Oban.Pro.Diagnostics, Oban.Pro.Refresher]
children = [
{Oban.Pro.Diagnostics, []},
{Oban.Pro.Refresher, []},
{Task.Supervisor, name: Oban.Pro.TaskSupervisor}
]
Supervisor.start_link(children, strategy: :one_for_one, name: __MODULE__)
end

View file

@ -517,6 +517,8 @@ defmodule Oban.Pro.Engines.Smart do
@registry Oban.Registry
@lock_codes ~w(deadlock_detected lock_not_available)a
@virtual_opts ~w(ack_async queue refresh_interval xact_delay xact_retry xact_timeout updated_at)a
@smart_opts Application.compile_env(:oban_pro, __MODULE__, [])
@ -793,9 +795,8 @@ defmodule Oban.Pro.Engines.Smart do
{:ok, {producer, jobs}}
{:error, _op, %{postgres: %{code: :unique_violation, detail: detail}}, changes} ->
clear_uniq_violation(changes.conf, detail, ~w(available executing retryable))
retry_unique_violation(:fetch_jobs, [conf, producer, running])
clear_unique_violation(changes.conf, detail, ~w(available executing retryable))
retry_unique_violation(conf, producer, running)
{:error, _operation, :xact_lock, _changes} ->
jittery_sleep()
@ -812,16 +813,35 @@ defmodule Oban.Pro.Engines.Smart do
end
catch
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
clear_uniq_violation(conf, detail, Enum.map(Job.states(), &to_string/1))
clear_unique_violation(conf, detail, Enum.map(Job.states(), &to_string/1))
retry_unique_violation(conf, producer, running)
retry_unique_violation(:fetch_jobs, [conf, producer, running])
:error, %Postgrex.Error{postgres: %{code: :deadlock_detected}} ->
:error, %Postgrex.Error{postgres: %{code: code}} when code in @lock_codes ->
jittery_sleep()
fetch_jobs(conf, producer, running)
end
defp retry_unique_violation(conf, producer, running) do
attempt = Process.get(:fetch_jobs, 0)
if attempt >= @max_uniq_retries do
Process.delete(:fetch_jobs)
Logger.error(fn ->
"[Oban.Pro.Engines.Smart] Unique violation retry limit (#{@max_uniq_retries}) exceeded " <>
"while fetching jobs. This may indicate a persistent unique constraint conflict that " <>
"can't be automatically resolved."
end)
{:ok, {producer, []}}
else
Process.put(:fetch_jobs, attempt + 1)
fetch_jobs(conf, producer, running)
end
end
@doc false
def fetch_for_drain(conf, %{queue: queue, with_limit: limit} = opts) do
subset_query =
@ -883,9 +903,14 @@ defmodule Oban.Pro.Engines.Smart do
{:ok, staged}
catch
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
clear_uniq_violation(conf, detail, ["scheduled", "retryable"])
# Staging runs inside the Stager's transaction, and this violation has already aborted
# it, so the conflict can't be repaired on this connection. Repair out-of-band and skip
# this round; the next staging pass promotes the freed jobs.
Utils.async_maybe_await(fn ->
clear_unique_violation(conf, detail, ["scheduled", "retryable"])
end)
retry_unique_violation(:stage_jobs, [conf, queryable, opts])
{:ok, []}
:error, %Postgrex.Error{postgres: %{code: :deadlock_detected}} ->
jittery_sleep()
@ -893,34 +918,6 @@ defmodule Oban.Pro.Engines.Smart do
stage_jobs(conf, queryable, opts)
end
defp retry_unique_violation(fun, args) do
attempt = Process.get(fun, 0)
if attempt >= @max_uniq_retries do
Process.delete(fun)
Logger.error(fn ->
"[Oban.Pro.Engines.Smart] Unique violation retry limit (#{@max_uniq_retries}) exceeded " <>
"in #{fun}. This may indicate a persistent unique constraint conflict that cannot " <>
"be automatically resolved."
end)
uniq_violation_failure(fun, args)
else
Process.put(fun, attempt + 1)
apply(__MODULE__, fun, args)
end
end
defp uniq_violation_failure(:fetch_jobs, [_conf, producer, _running]) do
{:ok, {producer, []}}
end
defp uniq_violation_failure(:stage_jobs, _args) do
{:ok, []}
end
@impl Engine
def check_available(%Config{} = conf) do
exists =
@ -1044,7 +1041,7 @@ defmodule Oban.Pro.Engines.Smart do
#
# We find conflicting jobs by matching the unique key in meta and filtering by the expected
# states. Clearing their `uniq_bmp` disables the unique constraint, allowing the transition.
def clear_uniq_violation(%Config{} = conf, detail, states) do
def clear_unique_violation(%Config{} = conf, detail, states) do
# Match both column-based `(uniq_key)=(value)` and expression-based `((meta->>'uniq_key'))=(value)`
# formats. The exact spacing is critical.
pattern = ~r/\((?:uniq_key|\(meta ->> 'uniq_key'::text\))\)=\((?<key>.+)\)/
@ -1192,7 +1189,7 @@ defmodule Oban.Pro.Engines.Smart do
mode =
case Repo.one(conf, query) do
nil -> :none
indexdef -> if String.contains?(indexdef, "(uniq_key)"), do: :column, else: :expression
indexdef -> if indexdef =~ ~r/\(uniq_key(?:\s+ASC)?\)/, do: :column, else: :expression
end
{:ok, mode}

View file

@ -10,6 +10,7 @@ defmodule Oban.Pro.Partition do
alias Oban.Pro.{Producer, Queue, Utils}
@keys_cache_ttl Application.compile_env(:oban_pro, [__MODULE__, :keys_cache_ttl], 3_000)
@conf_cache_ttl Application.compile_env(:oban_pro, [__MODULE__, :conf_cache_ttl], 60_000)
@keys_limit_mlt Application.compile_env(:oban_pro, [__MODULE__, :keys_limit_multiplier], 3)
@conf_tab Module.concat(__MODULE__, Conf)
@ -32,7 +33,15 @@ defmodule Oban.Pro.Partition do
# Clear the cache with a timeout, as not all queues are guaranteed to be running on a given
# node and the cache won't clear naturally.
:timer.apply_interval(@keys_cache_ttl, __MODULE__, :__clear_cache__, [@keys_cache_ttl])
:timer.apply_interval(@conf_cache_ttl, __MODULE__, :__clear_cache__, [
@conf_tab,
@conf_cache_ttl
])
:timer.apply_interval(@keys_cache_ttl, __MODULE__, :__clear_cache__, [
@keys_tab,
@keys_cache_ttl
])
:telemetry.attach(
"oban.pro.partition",
@ -47,12 +56,11 @@ defmodule Oban.Pro.Partition do
:telemetry.detach("oban.pro.partition")
end
def __clear_cache__(ttl) do
def __clear_cache__(tab, ttl) do
now = System.monotonic_time(:millisecond)
spec = {{:_, :_, :"$3"}, [{:>=, {:-, now, :"$3"}, ttl}], [true]}
:ets.select_delete(@keys_tab, [
{{:_, :_, :"$3"}, [{:>=, {:-, now, :"$3"}, ttl}], [true]}
])
:ets.select_delete(tab, [spec])
end
def handle_event(_event, _measure, %{conf: conf, key: key, meta: meta}, nil) do
@ -104,7 +112,7 @@ defmodule Oban.Pro.Partition do
@spec available_keys(Config.t(), String.t(), pos_integer()) :: [String.t()]
def available_keys(conf, queue, limit) do
timed_cache(conf.name, queue, fn ->
Utils.ets_cache(@keys_tab, {conf.name, queue}, @keys_cache_ttl, fn ->
query_limit = @keys_limit_mlt * limit
subquery =
@ -126,21 +134,6 @@ defmodule Oban.Pro.Partition do
end)
end
defp timed_cache(name, queue, fun) do
cache_key = {name, queue}
now = System.monotonic_time(:millisecond)
case :ets.lookup(@keys_tab, cache_key) do
[{^cache_key, keys, timestamp}] when now - timestamp < @keys_cache_ttl ->
keys
_ ->
keys = fun.()
:ets.insert(@keys_tab, {cache_key, keys, now})
keys
end
end
# Rescuing
@doc false
@ -208,20 +201,11 @@ defmodule Oban.Pro.Partition do
if Process.get(:oban_draining) do
:missing
else
case :ets.lookup(@conf_tab, {conf.name, queue}) do
[{_key, value}] ->
value
[] ->
value =
with :missing <- reg_config(conf, queue),
:missing <- pro_config(conf, queue),
do: dyn_config(conf, queue)
:ets.insert(@conf_tab, {{conf.name, queue}, value})
value
end
Utils.ets_cache(@conf_tab, {conf.name, queue}, @conf_cache_ttl, fn ->
with :missing <- reg_config(conf, queue),
:missing <- pro_config(conf, queue),
do: dyn_config(conf, queue)
end)
end
end

View file

@ -307,7 +307,7 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
Repo.update_all(state.conf, query, [], timeout: state.timeout)
catch
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
Smart.clear_uniq_violation(state.conf, detail, ~w(available executing retryable))
Smart.clear_unique_violation(state.conf, detail, ~w(available executing retryable))
{0, []}
end
@ -324,7 +324,7 @@ defmodule Oban.Pro.Plugins.DynamicLifeline do
Repo.update_all(state.conf, query, [], timeout: state.timeout)
catch
:error, %Postgrex.Error{postgres: %{code: :unique_violation, detail: detail}} ->
Smart.clear_uniq_violation(state.conf, detail, ~w(available executing retryable))
Smart.clear_unique_violation(state.conf, detail, ~w(available executing retryable))
{0, []}
end

View file

@ -60,7 +60,9 @@ defmodule Oban.Pro.Stages.Deadline do
dat = DateTime.from_unix!(at)
if :gt == DateTime.compare(now, dat) do
{:cancel, "deadline at #{inspect(dat)} exired"}
Hooks.execute_on_cancelled(:deadline, job)
{:cancel, "deadline at #{inspect(dat)} expired"}
else
if conf.force, do: force_timeout(now, dat, job)

View file

@ -14,6 +14,7 @@ defmodule Oban.Pro.Stages.Hooks do
after_cancelled: 2,
before_new: 2,
before_process: 1,
after_process: 2,
after_process: 3,
on_cancelled: 2,
on_discarded: 2
@ -128,7 +129,7 @@ defmodule Oban.Pro.Stages.Hooks do
all_hooks = Enum.uniq([worker | mod_hooks] ++ get_hooks())
for module <- all_hooks, hook_exported?(module, hook) do
apply(module, hook, args)
apply_hook(module, hook, args)
end
catch
kind, value ->
@ -137,6 +138,16 @@ defmodule Oban.Pro.Stages.Hooks do
end)
end
defp apply_hook(module, :after_process, [state, job, result]) do
if function_exported?(module, :after_process, 3) do
module.after_process(state, job, result)
else
module.after_process(state, job)
end
end
defp apply_hook(module, hook, args), do: apply(module, hook, args)
defp resolve_processing(job) do
with nil <- Process.get(:oban_processing),
{:ok, worker} <- Oban.Worker.from_string(job.worker),

View file

@ -71,7 +71,8 @@ defmodule Oban.Pro.Testing do
discarded: non_neg_integer(),
exhausted: non_neg_integer(),
retryable: non_neg_integer(),
scheduled: non_neg_integer()
scheduled: non_neg_integer(),
suspended: non_neg_integer()
}
@type drain_result :: drain_summary() | [Job.t()]
@ -201,7 +202,8 @@ defmodule Oban.Pro.Testing do
discarded: 0,
exhausted: 0,
retryable: 0,
scheduled: 0
scheduled: 0,
suspended: 0
}
@doc """

View file

@ -28,6 +28,20 @@ defmodule Oban.Pro.Utils do
|> Base.encode64(padding: false)
end
# Caching
def ets_cache(tab, key, ttl, fun) when is_function(fun, 0) do
now = System.monotonic_time(:millisecond)
case :ets.lookup(tab, key) do
[{^key, value, timestamp}] when now - timestamp < ttl ->
value
_ ->
tap(fun.(), &:ets.insert(tab, {key, &1, now}))
end
end
@spec persistent_cache(tuple(), (-> any())) :: any()
def persistent_cache(key, fun) when is_function(fun, 0) do
persistent_cache(key, :infinity, fun)
@ -45,6 +59,23 @@ defmodule Oban.Pro.Utils do
end
end
# Async Execution
def async_maybe_await(fun) when is_function(fun, 0) do
# Await during testing to prevent leaking side-effects after a test concludes.
if Process.get(:oban_draining) do
Oban.Pro.TaskSupervisor
|> Task.Supervisor.async(fun)
|> Task.await()
else
Task.Supervisor.start_child(Oban.Pro.TaskSupervisor, fun)
:ok
end
end
# Feature Detection
@spec has_legacy_workflows?(Oban.Config.t()) :: boolean()
def has_legacy_workflows?(conf) do
persistent_cache({__MODULE__, :legacy_workflows, conf.name}, @legacy_ttl, fn ->

View file

@ -203,10 +203,10 @@ defmodule Oban.Pro.Worker do
The example above uses `recorded: true` to opt into recording with the defaults. That means an
output `limit` of 64mb after compression and encodinganything larger than the configured limit
will return an error tuple. If you expect larger results (and you want them stored in the
database) you can override the limit. For example, to set the limit to 64mb instead:
database) you can override the limit. For example, to set the limit to 32mb instead:
```elixir
use Oban.Pro.Worker, recorded: [limit: 128_000_000]
use Oban.Pro.Worker, recorded: [limit: 32_000_000]
```
### Retrieving Results
@ -1561,18 +1561,16 @@ defmodule Oban.Pro.Worker do
end
defp await_signal(oban_name, id, wait_until, wait_timeout) do
case Notifier.listen(oban_name, :pro_signal) do
:ok ->
try do
await_loop(id, wait_until, wait_timeout)
after
Notifier.unlisten(oban_name, :pro_signal)
end
if Process.get(:oban_draining) do
park_or_timeout(wait_until)
else
Notifier.listen(oban_name, :pro_signal)
{:error, _reason} ->
# Without a notifier, e.g. while draining, there's no way to receive a live signal, so
# park immediately and let a buffered signal resume the job on a later pass.
park_or_timeout(wait_until)
try do
await_loop(id, wait_until, wait_timeout)
after
Notifier.unlisten(oban_name, :pro_signal)
end
end
end

View file

@ -1738,39 +1738,38 @@ defmodule Oban.Pro.Workflow do
sub_changesets =
Enum.map(sub_workflow.changesets, fn changeset ->
meta_deps = get_in(changeset.changes, [:meta, :deps])
context? = get_in(changeset.changes, [:meta, :context]) == true
nested? = get_in(changeset.changes, [:meta, :sup_workflow_id]) != nil
if deps != [] and meta_deps == [] do
old_ancestors = get_in(changeset.changes, [:meta, :ancestor_ids]) || []
new_ancestors = Map.get(workflow.opts, :ancestor_ids, [])
all_ancestors = old_ancestors ++ [workflow.id] ++ new_ancestors
old_ancestors = get_in(changeset.changes, [:meta, :ancestor_ids]) || []
new_ancestors = Map.get(workflow.opts, :ancestor_ids, [])
all_ancestors = old_ancestors ++ [workflow.id] ++ new_ancestors
opts
|> Map.new()
|> Map.put(:ancestor_ids, all_ancestors)
|> Map.put(:deps, deps)
|> Map.put(:sub_name, name)
|> Map.put(:sup_workflow_id, workflow.id)
|> Map.put(:workflow_id, sub_workflow.id)
|> put_hold(changeset)
else
new_ancestors = Map.get(workflow.opts, :ancestor_ids, [])
cond do
not context? and deps != [] and meta_deps == [] ->
opts
|> Map.new()
|> Map.put(:ancestor_ids, all_ancestors)
|> Map.put(:deps, deps)
|> Map.put(:sub_name, name)
|> Map.put(:sup_workflow_id, workflow.id)
|> Map.put(:workflow_id, sub_workflow.id)
|> put_hold(changeset)
meta =
if get_in(changeset.changes, [:meta, :sup_workflow_id]) do
old_ancestors = get_in(changeset.changes, [:meta, :ancestor_ids]) || []
all_ancestors = old_ancestors ++ [workflow.id] ++ new_ancestors
nested? ->
meta = %{sup_workflow_id: workflow.id, ancestor_ids: all_ancestors}
%{sup_workflow_id: workflow.id, ancestor_ids: all_ancestors}
else
%{
sub_name: name,
sup_workflow_id: workflow.id,
workflow_id: sub_workflow.id,
ancestor_ids: [workflow.id] ++ new_ancestors
}
end
Changeset.update_change(changeset, :meta, &Map.merge(&1, meta))
Changeset.update_change(changeset, :meta, &Map.merge(&1, meta))
true ->
meta = %{
sub_name: name,
sup_workflow_id: workflow.id,
workflow_id: sub_workflow.id,
ancestor_ids: [workflow.id] ++ new_ancestors
}
Changeset.update_change(changeset, :meta, &Map.merge(&1, meta))
end
end)
@ -1980,6 +1979,8 @@ defmodule Oban.Pro.Workflow do
_ -> graft_id
end
sub_id = workflow.id
changesets =
Enum.map(workflow.changesets, fn changeset ->
if get_in(changeset.changes, [:meta, :sup_workflow_id]) do
@ -1991,9 +1992,31 @@ defmodule Oban.Pro.Workflow do
workflow = %{workflow | id: root_id, changesets: changesets}
job
|> append(check_deps: false)
|> add_workflow(graft_name, workflow, opts)
grafted =
job
|> append(check_deps: false)
|> add_workflow(graft_name, workflow, opts)
changesets = Enum.map(grafted.changesets, &reparent_grafted(&1, root_id, sub_id))
%{grafted | changesets: changesets}
end
defp reparent_grafted(changeset, root_id, sub_id) do
case get_in(changeset.changes, [:meta, :workflow_id]) do
^root_id ->
changeset
_ ->
Changeset.update_change(changeset, :meta, fn meta ->
ancestors =
Enum.map(meta[:ancestor_ids] || [root_id], &if(&1 == sub_id, do: root_id, else: &1))
meta
|> Map.put(:sup_workflow_id, root_id)
|> Map.put(:ancestor_ids, ancestors)
end)
end
end
defp extract_graft! do

View file

@ -1,7 +1,7 @@
defmodule Oban.Pro.MixProject do
use Mix.Project
@version "1.7.6"
@version "1.7.7"
def project do
[

BIN
vendor/oban_web/.hex vendored

Binary file not shown.

View file

@ -95,6 +95,91 @@ options and burst mode configuration.
### Bug Fixes
- [Jobs] Restrict unauthorized job editing and updates with new permission
The save-job event handler previously dispatched changes from any client
without checking access controls, allowing a read-only user to rewrite a
job's worker module and potentially trigger code execution on the next
attempt. Editing now requires `:update_jobs` permission, which is
enabled by default for `:all` and disabled for `:read_only`.
- [Cron] Prevent malicious cron expressions from unrestricted memory allocation
A maliciously crafted cron expression like "0 0 1--100000000 \* \*" could
trigger multi-gigabyte allocations when `describe/1` eagerly expanded
the range during formatting. Range, value, and step parsing now validate
against per-field bounds and require ranges to be non-decreasing, so
out-of-domain inputs are rejected before any expansion occurs.
## v2.12.5 - 2026-07-06
### Enhancements
- [Jobs] Build jobs through worker `new/2` when available
The new job drawer always built changesets with `Job.new/2`, bypassing worker-level defaults,
validation, and Pro stages (recorded, chain, etc). Now we resolve the worker module and use its
`new/2` when it's loaded on the Web instance, falling back to `Job.new/2` when the module isn't
available.
### Bug Fixes
- [Dashboard] Resolve Elixir 1.20 compilation warnings
Fix all of the warnings surfaced by the Elixir v1.20 type checker and upgrade any packages with
errors or secutity warnings.
- [Job Details] Fix clearing tags when editing jobs
Treat blank tag input as an empty list when editing jobs, while leaving it as `nil` during job
creation.
- [Job Details] Add clipboard fallback for insecure contexts
The navigator.clipboard API is only available in secure contexts (HTTPS or localhost), so
copying job args, meta, and stacktraces failed with "navigator.clipboard is undefined" when Oban
Web was served over plain HTTP. Fall back to execCommand so copy actions work in those
environments.
- [Job Details] Fix new job form ignoring the scheduled at time
`DateTime.from_iso8601/1` returns a three-element tuple, but we only matched on `{:ok,
datetime}`. That clause never matched, so parsing always returned `nil` and jobs created with a
scheduled time ran immediately instead. Match the full tuple so the selected time is applied.
- [Job Details] Stack timeline labels on narrow screens
State boxes in the job timeline placed the state label and timestamp in a row that was too
narrow until the xl breakpoint, causing the timestamp to wrap awkwardly and the content to
bleed. Now the label and timestamp are stacked below xl, switching to side-by-side where there's
room to fit them.
- [Cron] Fix cron and tag history queries for CockroachDB
The cron history and tag suggestion queries relied on the Postgres-only implicit `value` column
name. CRDB names it after the function instead, which caused an `undefined_column` error.
Now the set-returning function is wrapped in a derived table with an explicit column alias for
all engines.
## v2.12.5 - 2026-05-26
### Enhancements
- [Jobs] Display awaitable signals in the job details page
Add a section that decodes and displays signal payloads sent via
`Oban.Pro.Worker.signal/2`. While a job is parked waiting, the section
shows "Awaiting Signal" with the deadline. Once a signal arrives, it
switches to "Received Signal".
- [Resolver] Add `format_signal/2` resolver callback
This allows customizing the decoded output,mirroring what's available
with `format_recorded/2`.
### Bug Fixes
- [Jobs] Restrict unauthorized job editing and updates with new permission
The save-job event handler previously dispatched changes from any client

View file

@ -1,109 +0,0 @@
{<<"links">>,
[{<<"Website">>,<<"https://oban.pro">>},
{<<"Changelog">>,
<<"https://github.com/oban-bg/oban_web/blob/main/CHANGELOG.md">>},
{<<"GitHub">>,<<"https://github.com/oban-bg/oban_web">>}]}.
{<<"name">>,<<"oban_web">>}.
{<<"version">>,<<"2.12.5">>}.
{<<"description">>,<<"Dashboard for the Oban job orchestration framework">>}.
{<<"elixir">>,<<"~> 1.15">>}.
{<<"files">>,
[<<"lib">>,<<"lib/mix">>,<<"lib/mix/tasks">>,
<<"lib/mix/tasks/oban_web.install.ex">>,<<"lib/oban">>,<<"lib/oban/web">>,
<<"lib/oban/web/cron.ex">>,<<"lib/oban/web/dashboard_live.ex">>,
<<"lib/oban/web/page.ex">>,<<"lib/oban/web/telemetry.ex">>,
<<"lib/oban/web/cache.ex">>,<<"lib/oban/web/plugins">>,
<<"lib/oban/web/plugins/stats.ex">>,<<"lib/oban/web/queue.ex">>,
<<"lib/oban/web/metrics.ex">>,<<"lib/oban/web/queries">>,
<<"lib/oban/web/queries/query_helpers.ex">>,
<<"lib/oban/web/queries/workflow_query.ex">>,
<<"lib/oban/web/queries/cron_query.ex">>,
<<"lib/oban/web/queries/job_query.ex">>,
<<"lib/oban/web/queries/queue_query.ex">>,<<"lib/oban/web/router.ex">>,
<<"lib/oban/web/components">>,<<"lib/oban/web/components/sort.ex">>,
<<"lib/oban/web/components/icons.ex">>,
<<"lib/oban/web/components/sidebar_components.ex">>,
<<"lib/oban/web/components/core.ex">>,
<<"lib/oban/web/components/layouts.ex">>,
<<"lib/oban/web/components/layouts">>,
<<"lib/oban/web/components/layouts/live.html.heex">>,
<<"lib/oban/web/components/layouts/root.html.heex">>,
<<"lib/oban/web/components/form_components.ex">>,
<<"lib/oban/web/workflow.ex">>,<<"lib/oban/web/timezones.ex">>,
<<"lib/oban/web/live">>,<<"lib/oban/web/live/search_component.ex">>,
<<"lib/oban/web/live/workflows">>,
<<"lib/oban/web/live/workflows/table_component.ex">>,
<<"lib/oban/web/live/workflows/detail_component.ex">>,
<<"lib/oban/web/live/shortcuts_component.ex">>,
<<"lib/oban/web/live/theme_component.ex">>,<<"lib/oban/web/live/queues">>,
<<"lib/oban/web/live/queues/table_component.ex">>,
<<"lib/oban/web/live/queues/detail_component.ex">>,
<<"lib/oban/web/live/queues/detail_instance_component.ex">>,
<<"lib/oban/web/live/help_component.ex">>,<<"lib/oban/web/live/crons">>,
<<"lib/oban/web/live/crons/table_component.ex">>,
<<"lib/oban/web/live/crons/new_component.ex">>,
<<"lib/oban/web/live/crons/detail_component.ex">>,
<<"lib/oban/web/live/crons/helpers.ex">>,<<"lib/oban/web/live/jobs">>,
<<"lib/oban/web/live/jobs/table_component.ex">>,
<<"lib/oban/web/live/jobs/sidebar_component.ex">>,
<<"lib/oban/web/live/jobs/new_component.ex">>,
<<"lib/oban/web/live/jobs/detail_component.ex">>,
<<"lib/oban/web/live/jobs/history_chart_component.ex">>,
<<"lib/oban/web/live/jobs/timeline_component.ex">>,
<<"lib/oban/web/live/jobs/chart_component.ex">>,
<<"lib/oban/web/live/connectivity_component.ex">>,
<<"lib/oban/web/live/instances_component.ex">>,
<<"lib/oban/web/live/refresh_component.ex">>,<<"lib/oban/web/helpers.ex">>,
<<"lib/oban/web/colors.ex">>,<<"lib/oban/web/cron_expr.ex">>,
<<"lib/oban/web/application.ex">>,<<"lib/oban/web/utils.ex">>,
<<"lib/oban/web/timing.ex">>,<<"lib/oban/web/pages">>,
<<"lib/oban/web/pages/jobs_page.ex">>,
<<"lib/oban/web/pages/crons_page.ex">>,
<<"lib/oban/web/pages/queues_page.ex">>,
<<"lib/oban/web/pages/workflows_page.ex">>,<<"lib/oban/web/resolver.ex">>,
<<"lib/oban/web/authentication.ex">>,<<"lib/oban/web/helpers">>,
<<"lib/oban/web/helpers/queue_helper.ex">>,<<"lib/oban/web/assets.ex">>,
<<"lib/oban/web/search.ex">>,<<"lib/oban/web/exceptions.ex">>,
<<"lib/oban/web.ex">>,<<"priv/static">>,<<"priv/static/app.css">>,
<<"priv/static/fonts">>,<<"priv/static/fonts/Inter.woff2">>,
<<"priv/static/app.js">>,<<"priv/timezones.txt">>,<<".formatter.exs">>,
<<"mix.exs">>,<<"README.md">>,<<"CHANGELOG.md">>,<<"LICENSE.txt">>]}.
{<<"app">>,<<"oban_web">>}.
{<<"licenses">>,[<<"Apache-2.0">>]}.
{<<"requirements">>,
[[{<<"name">>,<<"jason">>},
{<<"app">>,<<"jason">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.2">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"phoenix">>},
{<<"app">>,<<"phoenix">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.7">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"phoenix_html">>},
{<<"app">>,<<"phoenix_html">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 3.3 or ~> 4.0">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"phoenix_pubsub">>},
{<<"app">>,<<"phoenix_pubsub">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 2.1">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"phoenix_live_view">>},
{<<"app">>,<<"phoenix_live_view">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.0">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"oban">>},
{<<"app">>,<<"oban">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 2.21">>},
{<<"repository">>,<<"hexpm">>}],
[{<<"name">>,<<"oban_met">>},
{<<"app">>,<<"oban_met">>},
{<<"optional">>,false},
{<<"requirement">>,<<"~> 1.1">>},
{<<"repository">>,<<"hexpm">>}]]}.
{<<"build_tools">>,[<<"mix">>]}.

View file

@ -7,6 +7,8 @@ defmodule Oban.Web do
quote do
@moduledoc false
use Phoenix.Component
import Phoenix.Controller, only: [get_csrf_token: 0, view_module: 1, view_template: 1]
unquote(html_helpers())
@ -35,8 +37,6 @@ defmodule Oban.Web do
defp html_helpers do
quote do
use Phoenix.Component
import Oban.Web.Helpers
import Phoenix.HTML
import Phoenix.LiveView.Helpers

View file

@ -5,7 +5,7 @@ defmodule Oban.Web.Components.Icons do
attr :name, :string, required: true
attr :class, :any, default: nil
attr :rest, :global
attr :rest, :global, include: ~w(rel)
def icon(assigns) do
~H"""

View file

@ -423,7 +423,7 @@ defmodule Oban.Web.Crons.DetailComponent do
priority: new_val?(parse_int(params["priority"]), get_opt(cron, "priority")),
max_attempts: new_val?(parse_int(params["max_attempts"]), get_opt(cron, "max_attempts")),
guaranteed: new_val?(params["guaranteed"] == "true", get_opt(cron, "guaranteed") == true),
tags: new_val?(parse_tags(params["tags"]), get_opt(cron, "tags")),
tags: new_val?(parse_form_tags(params), get_opt(cron, "tags")),
args: new_val?(parse_json(params["args"]), get_opt(cron, "args"))
]
end
@ -435,6 +435,9 @@ defmodule Oban.Web.Crons.DetailComponent do
defp new_val?(val, nil, val), do: nil
defp new_val?(val, _current, _default), do: val
defp parse_form_tags(%{"tags" => tags}), do: parse_tags(tags) || []
defp parse_form_tags(_params), do: nil
defp get_opt(%{opts: opts}, key) do
Map.get(opts, key)
end

View file

@ -79,8 +79,7 @@ defmodule Oban.Web.Jobs.ChartComponent do
class="w-3 h-3 ml-1 bg-violet-300 rounded-full"
data-title={"Filtered by #{params_to_filters_list(@params)}"}
phx-hook="Tippy"
>
</span>
></span>
</div>
<div id="chart-c" class="flex space-x-2">

View file

@ -1021,7 +1021,7 @@ defmodule Oban.Web.Jobs.DetailComponent do
priority: new_val?(parse_int(params["priority"]), job.priority),
max_attempts: new_val?(parse_int(params["max_attempts"]), job.max_attempts),
scheduled_at: new_val?(parse_datetime(params["scheduled_at"]), job.scheduled_at),
tags: new_val?(parse_tags(params["tags"]), job.tags),
tags: new_val?(parse_edit_tags(params), job.tags),
args: new_val?(parse_json(params["args"]), job.args)
]
end
@ -1031,6 +1031,9 @@ defmodule Oban.Web.Jobs.DetailComponent do
defp new_val?(val, val), do: nil
defp new_val?(val, _current), do: val
defp parse_edit_tags(%{"tags" => tags}), do: parse_tags(tags) || []
defp parse_edit_tags(_params), do: nil
defp parse_datetime(nil), do: nil
defp parse_datetime(""), do: nil

View file

@ -151,7 +151,7 @@ defmodule Oban.Web.Jobs.NewComponent do
with {:ok, worker} <- parse_worker(params["worker"]),
{:ok, args} <- parse_args(params["args"]),
{:ok, opts} <- build_opts(params) do
changeset = Oban.Job.new(args, [{:worker, worker} | opts])
changeset = build_changeset(worker, args, opts)
case Oban.insert(conf.name, changeset) do
{:ok, job} ->
@ -227,6 +227,13 @@ defmodule Oban.Web.Jobs.NewComponent do
defp parse_worker(_), do: {:error, "Worker is required"}
defp build_changeset(worker, args, opts) do
case Oban.Worker.from_string(worker) do
{:ok, module} -> module.new(args, opts)
{:error, _reason} -> Oban.Job.new(args, [{:worker, worker} | opts])
end
end
defp parse_args(args) when is_binary(args) do
case parse_json(args) do
nil -> {:error, "Args must be a valid JSON object"}
@ -273,7 +280,7 @@ defmodule Oban.Web.Jobs.NewComponent do
defp parse_scheduled_at(str) when is_binary(str) do
case DateTime.from_iso8601(str <> ":00Z") do
{:ok, datetime} -> datetime
{:ok, datetime, _offset} -> datetime
_ -> nil
end
end

View file

@ -73,7 +73,7 @@ defmodule Oban.Web.Jobs.TimelineComponent do
~H"""
<div
class={"flex flex-col sm:flex-row items-center sm:justify-between h-12 sm:h-12 rounded-lg border-2 px-2 sm:px-3 #{@border_class} #{@bg_class} #{@dim_class}"}
class={"flex flex-col xl:flex-row items-center justify-center xl:justify-between h-12 rounded-lg border-2 px-2 sm:px-3 #{@border_class} #{@bg_class} #{@dim_class}"}
data-title={@tooltip}
phx-hook="Tippy"
id={"timeline-#{@state}"}
@ -86,7 +86,7 @@ defmodule Oban.Web.Jobs.TimelineComponent do
{@state}
</span>
</div>
<span class={"hidden sm:inline text-xs tabular-nums #{@text_color}"}>
<span class={"hidden sm:inline text-xs tabular-nums whitespace-nowrap #{@text_color}"}>
{@timestamp || ""}
</span>
</div>

View file

@ -335,7 +335,7 @@ defmodule Oban.Web.JobsPage do
send(self(), :cancel_selected)
end
{:noreply, assign(socket, expanded?: false)}
{:noreply, socket}
end
def handle_event("retry-jobs", _params, socket) do
@ -343,7 +343,7 @@ defmodule Oban.Web.JobsPage do
send(self(), :retry_selected)
end
{:noreply, assign(socket, expanded?: false)}
{:noreply, socket}
end
def handle_event("delete-jobs", _params, socket) do
@ -351,10 +351,10 @@ defmodule Oban.Web.JobsPage do
send(self(), :delete_selected)
end
{:noreply, assign(socket, expanded?: false)}
{:noreply, socket}
end
# Queues
# System
@impl Page
def handle_info({ref, _val}, socket) when is_reference(ref) do
@ -365,14 +365,6 @@ defmodule Oban.Web.JobsPage do
{:noreply, socket}
end
def handle_info({:scale_queue, queue, limit}, socket) do
Telemetry.action(:scale_queue, socket, [queue: queue, limit: limit], fn ->
Oban.scale_queue(socket.assigns.conf.name, queue: queue, limit: limit)
end)
{:noreply, socket}
end
def handle_info({:flash, mode, message}, socket) do
{:noreply, put_flash_with_clear(socket, mode, message)}
end
@ -410,14 +402,6 @@ defmodule Oban.Web.JobsPage do
{:noreply, assign(socket, detailed: job)}
end
def handle_info({:delete_job, job}, socket) do
Telemetry.action(:delete_jobs, socket, [job_ids: [job.id]], fn ->
JobQuery.delete_jobs(socket.assigns.conf, [job.id])
end)
{:noreply, push_patch(socket, to: oban_path(:jobs), replace: true)}
end
def handle_info({:retry_job, job}, socket) do
Telemetry.action(:retry_jobs, socket, [job_ids: [job.id]], fn ->
JobQuery.retry_jobs(socket.assigns.conf, [job.id])
@ -428,6 +412,14 @@ defmodule Oban.Web.JobsPage do
{:noreply, assign(socket, detailed: job)}
end
def handle_info({:delete_job, job}, socket) do
Telemetry.action(:delete_jobs, socket, [job_ids: [job.id]], fn ->
JobQuery.delete_jobs(socket.assigns.conf, [job.id])
end)
{:noreply, push_patch(socket, to: oban_path(:jobs), replace: true)}
end
def handle_info({:update_job, job, changes}, socket) do
conf = socket.assigns.conf
@ -445,38 +437,7 @@ defmodule Oban.Web.JobsPage do
end
end
# Selection
def handle_info({:toggle_select, job_id}, socket) do
selected = socket.assigns.selected
selected =
if MapSet.member?(selected, job_id) do
MapSet.delete(selected, job_id)
else
MapSet.put(selected, job_id)
end
{:noreply, assign(socket, selected: selected)}
end
def handle_info(:toggle_select_all, socket) do
selected =
if Enum.any?(socket.assigns.selected) do
MapSet.new()
else
# Always include the jobs we can see currently to compensate for slower refresh rates.
# Without this, visible jobs may not be selected and the interface looks broken.
local_set = MapSet.new(socket.assigns.jobs, & &1.id)
socket.assigns.params
|> JobQuery.all_job_ids(socket.assigns.conf)
|> MapSet.new()
|> MapSet.union(local_set)
end
{:noreply, assign(socket, selected: selected)}
end
# Bulk Actions
def handle_info(:cancel_selected, socket) do
job_ids = MapSet.to_list(socket.assigns.selected)
@ -523,6 +484,39 @@ defmodule Oban.Web.JobsPage do
{:noreply, handle_refresh(socket)}
end
# Selection
def handle_info({:toggle_select, job_id}, socket) do
selected = socket.assigns.selected
selected =
if MapSet.member?(selected, job_id) do
MapSet.delete(selected, job_id)
else
MapSet.put(selected, job_id)
end
{:noreply, assign(socket, selected: selected)}
end
def handle_info(:toggle_select_all, socket) do
selected =
if Enum.any?(socket.assigns.selected) do
MapSet.new()
else
# Always include the jobs we can see currently to compensate for slower refresh rates.
# Without this, visible jobs may not be selected and the interface looks broken.
local_set = MapSet.new(socket.assigns.jobs, & &1.id)
socket.assigns.params
|> JobQuery.all_job_ids(socket.assigns.conf)
|> MapSet.new()
|> MapSet.union(local_set)
end
{:noreply, assign(socket, selected: selected)}
end
# Param Helpers
defp params_with_defaults(params, socket) do

View file

@ -325,6 +325,8 @@ defmodule Oban.Web.QueuesPage do
{:noreply, socket}
end
# Single Actions
@impl Page
def handle_info({:pause_queue, queue}, socket) do
Telemetry.action(:pause_queue, socket, [queue: queue], fn ->
@ -358,6 +360,18 @@ defmodule Oban.Web.QueuesPage do
{:noreply, socket}
end
def handle_info({:stop_queue, queue}, socket) do
enforce_access!(:stop_queues, socket.assigns.access)
Telemetry.action(:stop_queue, socket, [queue: queue], fn ->
Oban.stop_queue(socket.assigns.conf.name, queue: queue)
end)
{:noreply, put_flash_with_clear(socket, :info, "Queue #{queue} stopped")}
end
# Bulk Actions
def handle_info(:pause_queues, socket) do
enforce_access!(:pause_queues, socket.assigns.access)
@ -409,41 +423,7 @@ defmodule Oban.Web.QueuesPage do
{:noreply, socket}
end
def handle_info({:stop_queue, queue}, socket) do
enforce_access!(:stop_queues, socket.assigns.access)
Telemetry.action(:stop_queue, socket, [queue: queue], fn ->
Oban.stop_queue(socket.assigns.conf.name, queue: queue)
end)
{:noreply, put_flash_with_clear(socket, :info, "Queue #{queue} stopped")}
end
def handle_info({:toggle_select, queue}, socket) do
selected = socket.assigns.selected
selected =
if MapSet.member?(selected, queue) do
MapSet.delete(selected, queue)
else
MapSet.put(selected, queue)
end
{:noreply, assign(socket, selected: selected)}
end
def handle_info(:toggle_select_all, socket) do
selected =
if Enum.any?(socket.assigns.selected) do
MapSet.new()
else
socket.assigns.params
|> QueueQuery.all_queues(socket.assigns.conf)
|> MapSet.new(& &1.name)
end
{:noreply, assign(socket, selected: selected)}
end
# Scaling
def handle_info({:scale_queue, queue, name, node, limit}, socket) do
meta = [queue: queue, name: name, node: node, limit: limit]
@ -474,6 +454,36 @@ defmodule Oban.Web.QueuesPage do
{:noreply, put_flash_with_clear(socket, :info, scale_message(queue, opts))}
end
# Selection
def handle_info({:toggle_select, queue}, socket) do
selected = socket.assigns.selected
selected =
if MapSet.member?(selected, queue) do
MapSet.delete(selected, queue)
else
MapSet.put(selected, queue)
end
{:noreply, assign(socket, selected: selected)}
end
def handle_info(:toggle_select_all, socket) do
selected =
if Enum.any?(socket.assigns.selected) do
MapSet.new()
else
socket.assigns.params
|> QueueQuery.all_queues(socket.assigns.conf)
|> MapSet.new(& &1.name)
end
{:noreply, assign(socket, selected: selected)}
end
# Filtering
def handle_info({:params, :limit, inc}, socket) when is_integer(inc) do
params =
socket.assigns.params
@ -483,6 +493,8 @@ defmodule Oban.Web.QueuesPage do
{:noreply, push_patch(socket, to: oban_path(:queues, params), replace: true)}
end
# Fallback
def handle_info(_, socket) do
{:noreply, socket}
end

View file

@ -296,7 +296,7 @@ defmodule Oban.Web.CronQuery do
ranked = from t in subquery(inside), where: t.rn <= @history_limit
query =
from f in fragment("json_array_elements_text(?)", ^names),
from f in fragment("(SELECT value FROM json_array_elements_text(?) AS t(value))", ^names),
as: :list,
left_lateral_join: j in subquery(ranked),
on: true,

View file

@ -56,7 +56,12 @@ defmodule Oban.Web.JobQuery do
defmacrop json_unnest(field) do
quote do
fragment("json_array_elements_text(array_to_json(?))", unquote(field))
fragment(
"""
(SELECT value FROM json_array_elements_text(array_to_json(?)) AS t(value))
""",
unquote(field)
)
end
end
@ -324,7 +329,7 @@ defmodule Oban.Web.JobQuery do
join(query, :inner, [j], x in json_table(j.tags), on: true)
true ->
join(query, :inner, [j], x in json_unnest(j.tags), on: true)
join(query, :inner_lateral, [j], x in json_unnest(j.tags), on: true)
end
query =

View file

@ -2,7 +2,7 @@ defmodule Oban.Web.MixProject do
use Mix.Project
@source_url "https://github.com/oban-bg/oban_web"
@version "2.12.5"
@version "2.12.6"
def project do
[

File diff suppressed because one or more lines are too long

View file

@ -166,7 +166,7 @@
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<span>Showing first ${t} jobs. This workflow has more jobs than can be displayed.</span>
`,this.el.appendChild(r)},updateControlColors(){let e=this.el.querySelector("#graph-controls");if(!e)return;let t=this.isDarkMode(),n=t?"bg-gray-800":"bg-white",i=t?"border-gray-600":"border-gray-300",r=t?"text-gray-300":"text-gray-600",s=t?"hover:bg-gray-700":"hover:bg-gray-100";e.querySelectorAll("button").forEach(o=>{if(o.id==="toggle-tracking"&&this.trackActiveNode){let a=t?"bg-blue-900":"bg-blue-100",l=t?"border-blue-500":"border-blue-400",c=t?"text-blue-400":"text-blue-600";o.className=`w-8 h-8 flex items-center justify-center rounded-md border transition-colors cursor-pointer ${a} ${l} ${c}`}else o.className=`w-8 h-8 flex items-center justify-center rounded-md border transition-colors cursor-pointer ${n} ${i} ${r} ${s}`})}},Uw=$V;var WV={Completer:Ac,CronChart:$d,CronSparkline:$h,JobHistoryChart:Vd,JobsChart:Gd,HistoryBack:Xd,Instantiator:Kd,QueueDetailChart:Zd,QueueSparkline:Wh,Refresher:Jd,Relativize:Qd,Shortcuts:th,SidebarResizer:eh,Themer:Vh,TimelineConnectors:Hh,Tippy:Uh,WorkflowGraph:Uw},So;Mo.default.config({barColors:{0:"#0284c7"},shadowColor:"rgba(0, 0, 0, .3)"});window.addEventListener("phx:page-loading-start",e=>{So||(So=setTimeout(()=>Mo.default.show(),500))});window.addEventListener("phx:page-loading-stop",e=>{clearTimeout(So),So=void 0,Mo.default.hide()});window.addEventListener("phx:scroll-top",()=>{window.scrollTo({top:0,behavior:"instant"})});window.addEventListener("phx:copy-to-clipboard",e=>{let t=e.detail.text;t&&navigator.clipboard.writeText(t)});var VV=document.querySelector("meta[name='csrf-token']").getAttribute("content"),HV=document.querySelector("meta[name='live-transport']").getAttribute("content"),UV=document.querySelector("meta[name='live-path']").getAttribute("content"),YV=new LiveView.LiveSocket(UV,Phoenix.Socket,{transport:HV==="longpoll"?Phoenix.LongPoll:WebSocket,params:{_csrf_token:VV,init_state:Tc()},hooks:WV});YV.connect();})();
`,this.el.appendChild(r)},updateControlColors(){let e=this.el.querySelector("#graph-controls");if(!e)return;let t=this.isDarkMode(),n=t?"bg-gray-800":"bg-white",i=t?"border-gray-600":"border-gray-300",r=t?"text-gray-300":"text-gray-600",s=t?"hover:bg-gray-700":"hover:bg-gray-100";e.querySelectorAll("button").forEach(o=>{if(o.id==="toggle-tracking"&&this.trackActiveNode){let a=t?"bg-blue-900":"bg-blue-100",l=t?"border-blue-500":"border-blue-400",c=t?"text-blue-400":"text-blue-600";o.className=`w-8 h-8 flex items-center justify-center rounded-md border transition-colors cursor-pointer ${a} ${l} ${c}`}else o.className=`w-8 h-8 flex items-center justify-center rounded-md border transition-colors cursor-pointer ${n} ${i} ${r} ${s}`})}},Uw=$V;var WV={Completer:Ac,CronChart:$d,CronSparkline:$h,JobHistoryChart:Vd,JobsChart:Gd,HistoryBack:Xd,Instantiator:Kd,QueueDetailChart:Zd,QueueSparkline:Wh,Refresher:Jd,Relativize:Qd,Shortcuts:th,SidebarResizer:eh,Themer:Vh,TimelineConnectors:Hh,Tippy:Uh,WorkflowGraph:Uw},So;Mo.default.config({barColors:{0:"#0284c7"},shadowColor:"rgba(0, 0, 0, .3)"});window.addEventListener("phx:page-loading-start",e=>{So||(So=setTimeout(()=>Mo.default.show(),500))});window.addEventListener("phx:page-loading-stop",e=>{clearTimeout(So),So=void 0,Mo.default.hide()});window.addEventListener("phx:scroll-top",()=>{window.scrollTo({top:0,behavior:"instant"})});window.addEventListener("phx:copy-to-clipboard",e=>{let t=e.detail.text;if(navigator.clipboard)navigator.clipboard.writeText(t);else{let n=document.createElement("textarea");n.value=t,n.style.position="fixed",n.style.opacity="0",document.body.appendChild(n),n.focus(),n.select();try{document.execCommand("copy")}finally{document.body.removeChild(n)}}});var VV=document.querySelector("meta[name='csrf-token']").getAttribute("content"),HV=document.querySelector("meta[name='live-transport']").getAttribute("content"),UV=document.querySelector("meta[name='live-path']").getAttribute("content"),YV=new LiveView.LiveSocket(UV,Phoenix.Socket,{transport:HV==="longpoll"?Phoenix.LongPoll:WebSocket,params:{_csrf_token:VV,init_state:Tc()},hooks:WV});YV.connect();})();
/*!
* @kurkle/color v0.3.2
* https://github.com/kurkle/color#readme