blob: e6ff691d9f634cdfa6a0707914073d9014bef319 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
defmodule Fluss.Test.Cluster do
@moduledoc false
# Shells out to the `fluss-test-cluster` CLI (from `crates/fluss-test-cluster`),
# the same binary used by the Python and C++ integration tests.
@cluster_name "shared-test"
@cluster_json_prefix "CLUSTER_JSON: "
def ensure_started do
case System.get_env("FLUSS_BOOTSTRAP_SERVERS") do
nil -> start_cluster()
servers -> {:ok, servers}
end
end
def stop do
if System.get_env("FLUSS_BOOTSTRAP_SERVERS") do
:ok
else
case find_cli_binary() do
{:ok, cli} ->
System.cmd(cli, ["stop", "--name", @cluster_name], stderr_to_stdout: true)
:ok
{:error, _} ->
:ok
end
end
end
def connect_with_retry(config, timeout_s) do
deadline = System.monotonic_time(:second) + timeout_s
do_connect_retry(config, deadline, nil)
end
defp do_connect_retry(config, deadline, last_error) do
if System.monotonic_time(:second) >= deadline do
raise "Could not connect to Fluss cluster: #{inspect(last_error)}"
end
try do
conn = Fluss.Connection.new!(config)
admin = Fluss.Admin.new!(conn)
{:ok, _databases} = Fluss.Admin.list_databases(admin)
{conn, admin}
rescue
e ->
Process.sleep(2_000)
do_connect_retry(config, deadline, e)
end
end
defp start_cluster do
with {:ok, cli} <- find_cli_binary(),
{output, 0} <-
System.cmd(cli, ["start", "--sasl", "--name", @cluster_name], stderr_to_stdout: true),
{:ok, bootstrap} <- parse_cluster_json(output) do
{:ok, bootstrap}
else
{output, code} when is_binary(output) ->
{:error, "fluss-test-cluster start failed (exit #{code}):\n#{output}"}
{:error, _} = err ->
err
end
end
defp find_cli_binary do
case System.get_env("FLUSS_TEST_CLUSTER_BIN") do
bin when is_binary(bin) and bin != "" ->
if File.regular?(bin),
do: {:ok, bin},
else: {:error, "FLUSS_TEST_CLUSTER_BIN=#{bin} does not exist"}
_ ->
locate_via_cargo()
end
end
defp locate_via_cargo do
case System.cmd("cargo", ["locate-project", "--workspace", "--message-format", "plain"],
stderr_to_stdout: true
) do
{output, 0} ->
output |> String.trim() |> Path.dirname() |> find_binary_in_target()
{output, code} ->
{:error, "cargo locate-project failed (exit #{code}): #{output}"}
end
end
defp find_binary_in_target(root) do
Enum.find_value(
["debug", "release"],
{:error, "fluss-test-cluster binary not found. Run: cargo build -p fluss-test-cluster"},
&check_binary(root, &1)
)
end
defp check_binary(root, profile) do
path = Path.join([root, "target", profile, "fluss-test-cluster"])
if File.regular?(path), do: {:ok, path}, else: nil
end
defp parse_cluster_json(output) do
output
|> String.split("\n", trim: true)
|> Enum.find_value(
{:error, "No #{@cluster_json_prefix} token in output:\n#{output}"},
&extract_bootstrap/1
)
end
defp extract_bootstrap(line) do
case String.split(line, @cluster_json_prefix, parts: 2) do
[_, json] ->
case decode_bootstrap(json) do
{:ok, bootstrap} -> {:ok, bootstrap}
_ -> nil
end
_ ->
nil
end
end
# Minimal JSON extractor for `bootstrap_servers`: avoids adding a JSON dep just for tests.
defp decode_bootstrap(json) do
case Regex.run(~r/"bootstrap_servers"\s*:\s*"([^"]+)"/, json) do
[_, servers] -> {:ok, servers}
_ -> {:error, "no bootstrap_servers in: #{json}"}
end
end
end