1818defmodule Fluss.Test.Cluster do
1919 @ moduledoc false
2020
21- @ fluss_image "apache /fluss"
22- @ fluss_version "0.9.0-incubating"
21+ # Shells out to the `fluss-test-cluster` CLI (from `crates /fluss-test-cluster`),
22+ # the same binary used by the Python and C++ integration tests.
2323
24- @ network_name "fluss-elixir-test-network"
25- @ zookeeper_name "zookeeper-elixir-test"
26- @ coordinator_name "coordinator-server-elixir-test"
27- @ tablet_server_name "tablet-server-elixir-test"
28-
29- # Same fixed ports used by Python/C++ integration tests.
30- @ coordinator_sasl_port 9123
31- @ coordinator_plain_port 9223
32- @ tablet_sasl_port 9124
33- @ tablet_plain_port 9224
34-
35- def bootstrap_servers , do: "127.0.0.1:#{ @ coordinator_plain_port } "
24+ @ cluster_name "shared-test"
25+ @ cluster_json_prefix "CLUSTER_JSON: "
3626
3727 def ensure_started do
3828 case System . get_env ( "FLUSS_BOOTSTRAP_SERVERS" ) do
@@ -42,170 +32,99 @@ defmodule Fluss.Test.Cluster do
4232 end
4333
4434 def stop do
45- for name <- [ @ tablet_server_name , @ coordinator_name , @ zookeeper_name ] do
46- System . cmd ( "docker" , [ "rm" , "-f" , name ] , stderr_to_stdout: true )
47- end
35+ if System . get_env ( "FLUSS_BOOTSTRAP_SERVERS" ) do
36+ :ok
37+ else
38+ case find_cli_binary ( ) do
39+ { :ok , cli } ->
40+ System . cmd ( cli , [ "stop" , "--name" , @ cluster_name ] , stderr_to_stdout: true )
41+ :ok
4842
49- System . cmd ( "docker" , [ "network" , "rm" , @ network_name ] , stderr_to_stdout: true )
50- :ok
43+ { :error , _ } ->
44+ :ok
45+ end
46+ end
5147 end
5248
5349 defp start_cluster do
54- if port_open? ( @ coordinator_plain_port ) do
55- IO . puts ( "Reusing existing Fluss cluster on port #{ @ coordinator_plain_port } " )
56- { :ok , bootstrap_servers ( ) }
50+ with { :ok , cli } <- find_cli_binary ( ) ,
51+ { output , 0 } <-
52+ System . cmd ( cli , [ "start" , "--sasl" , "--name" , @ cluster_name ] , stderr_to_stdout: true ) ,
53+ { :ok , bootstrap } <- parse_cluster_json ( output ) do
54+ { :ok , bootstrap }
5755 else
58- do_start_cluster ( )
56+ { output , code } when is_binary ( output ) ->
57+ { :error , "fluss-test-cluster start failed (exit #{ code } ):\n #{ output } " }
58+
59+ { :error , _ } = err ->
60+ err
5961 end
6062 end
6163
62- defp do_start_cluster do
63- IO . puts ( "Starting Fluss cluster via Docker..." )
64+ defp find_cli_binary do
65+ case System . get_env ( "FLUSS_TEST_CLUSTER_BIN" ) do
66+ bin when is_binary ( bin ) and bin != "" ->
67+ if File . regular? ( bin ) ,
68+ do: { :ok , bin } ,
69+ else: { :error , "FLUSS_TEST_CLUSTER_BIN=#{ bin } does not exist" }
6470
65- # Remove any leftover containers from previous runs
66- for name <- [ @ tablet_server_name , @ coordinator_name , @ zookeeper_name ] do
67- System . cmd ( "docker" , [ "rm" , "-f" , name ] , stderr_to_stdout: true )
68- end
69-
70- System . cmd ( "docker" , [ "network" , "create" , @ network_name ] , stderr_to_stdout: true )
71-
72- sasl_jaas =
73- ~s( org.apache.fluss.security.auth.sasl.plain.PlainLoginModule required user_admin="admin-secret" user_alice="alice-secret";)
74-
75- coordinator_props =
76- Enum . join (
77- [
78- "zookeeper.address: #{ @ zookeeper_name } :2181" ,
79- "bind.listeners: INTERNAL://#{ @ coordinator_name } :0, CLIENT://#{ @ coordinator_name } :9123, PLAIN_CLIENT://#{ @ coordinator_name } :9223" ,
80- "advertised.listeners: CLIENT://localhost:#{ @ coordinator_sasl_port } , PLAIN_CLIENT://localhost:#{ @ coordinator_plain_port } " ,
81- "internal.listener.name: INTERNAL" ,
82- "security.protocol.map: CLIENT:sasl" ,
83- "security.sasl.enabled.mechanisms: plain" ,
84- "security.sasl.plain.jaas.config: #{ sasl_jaas } " ,
85- "netty.server.num-network-threads: 1" ,
86- "netty.server.num-worker-threads: 3"
87- ] ,
88- "\n "
89- )
90-
91- tablet_props =
92- Enum . join (
93- [
94- "zookeeper.address: #{ @ zookeeper_name } :2181" ,
95- "bind.listeners: INTERNAL://#{ @ tablet_server_name } :0, CLIENT://#{ @ tablet_server_name } :9123, PLAIN_CLIENT://#{ @ tablet_server_name } :9223" ,
96- "advertised.listeners: CLIENT://localhost:#{ @ tablet_sasl_port } , PLAIN_CLIENT://localhost:#{ @ tablet_plain_port } " ,
97- "internal.listener.name: INTERNAL" ,
98- "security.protocol.map: CLIENT:sasl" ,
99- "security.sasl.enabled.mechanisms: plain" ,
100- "security.sasl.plain.jaas.config: #{ sasl_jaas } " ,
101- "tablet-server.id: 0" ,
102- "netty.server.num-network-threads: 1" ,
103- "netty.server.num-worker-threads: 3"
104- ] ,
105- "\n "
106- )
107-
108- docker_run ( [
109- "--name" ,
110- @ zookeeper_name ,
111- "--network" ,
112- @ network_name ,
113- "-d" ,
114- "zookeeper:3.9.2"
115- ] )
116-
117- docker_run ( [
118- "--name" ,
119- @ coordinator_name ,
120- "--network" ,
121- @ network_name ,
122- "-p" ,
123- "#{ @ coordinator_sasl_port } :9123" ,
124- "-p" ,
125- "#{ @ coordinator_plain_port } :9223" ,
126- "-e" ,
127- "FLUSS_PROPERTIES=#{ coordinator_props } " ,
128- "-d" ,
129- "#{ @ fluss_image } :#{ @ fluss_version } " ,
130- "coordinatorServer"
131- ] )
132-
133- docker_run ( [
134- "--name" ,
135- @ tablet_server_name ,
136- "--network" ,
137- @ network_name ,
138- "-p" ,
139- "#{ @ tablet_sasl_port } :9123" ,
140- "-p" ,
141- "#{ @ tablet_plain_port } :9223" ,
142- "-e" ,
143- "FLUSS_PROPERTIES=#{ tablet_props } " ,
144- "-d" ,
145- "#{ @ fluss_image } :#{ @ fluss_version } " ,
146- "tabletServer"
147- ] )
148-
149- all_ports = [ @ coordinator_plain_port , @ tablet_plain_port ]
150-
151- if wait_for_ports ( all_ports , 90 ) do
152- IO . puts ( "Fluss cluster started successfully." )
153- { :ok , bootstrap_servers ( ) }
154- else
155- { :error , "Cluster ports did not become ready within timeout" }
71+ _ ->
72+ locate_via_cargo ( )
15673 end
15774 end
15875
159- defp docker_run ( args ) do
160- { output , code } = System . cmd ( "docker" , [ "run" | args ] , stderr_to_stdout: true )
76+ defp locate_via_cargo do
77+ case System . cmd ( "cargo" , [ "locate-project" , "--workspace" , "--message-format" , "plain" ] ,
78+ stderr_to_stdout: true
79+ ) do
80+ { output , 0 } ->
81+ output |> String . trim ( ) |> Path . dirname ( ) |> find_binary_in_target ( )
16182
162- if code != 0 do
163- IO . puts ( "Docker run warning (code #{ code } ): #{ output } ")
83+ { output , code } ->
84+ { :error , "cargo locate-project failed (exit #{ code } ): #{ output } "}
16485 end
16586 end
16687
167- defp wait_for_ports ( ports , timeout_s ) do
168- deadline = System . monotonic_time ( :second ) + timeout_s
88+ defp find_binary_in_target ( root ) do
89+ Enum . find_value (
90+ [ "debug" , "release" ] ,
91+ { :error , "fluss-test-cluster binary not found. Run: cargo build -p fluss-test-cluster" } ,
92+ & check_binary ( root , & 1 )
93+ )
94+ end
16995
170- Enum . all? ( ports , fn port ->
171- remaining = deadline - System . monotonic_time ( :second )
172- remaining > 0 and wait_for_port ( port , remaining )
173- end )
96+ defp check_binary ( root , profile ) do
97+ path = Path . join ( [ root , "target" , profile , "fluss-test-cluster" ] )
98+ if File . regular? ( path ) , do: { :ok , path } , else: nil
17499 end
175100
176- defp wait_for_port ( port , timeout_s ) do
177- deadline = System . monotonic_time ( :second ) + timeout_s
101+ defp parse_cluster_json ( output ) do
102+ output
103+ |> String . split ( "\n " , trim: true )
104+ |> Enum . find_value (
105+ { :error , "No #{ @ cluster_json_prefix } token in output:\n #{ output } " } ,
106+ & extract_bootstrap / 1
107+ )
108+ end
178109
179- Stream . repeatedly ( fn ->
180- case :gen_tcp . connect ( ~c" localhost" , port , [ ] , 1000 ) do
181- { :ok , socket } ->
182- :gen_tcp . close ( socket )
183- :ok
110+ defp extract_bootstrap ( line ) do
111+ case String . split ( line , @ cluster_json_prefix , parts: 2 ) do
112+ [ _ , json ] ->
113+ case decode_bootstrap ( json ) do
114+ { :ok , bootstrap } -> { :ok , bootstrap }
115+ _ -> nil
116+ end
184117
185- { :error , _ } ->
186- Process . sleep ( 1000 )
187- :retry
188- end
189- end )
190- |> Enum . reduce_while ( false , fn
191- :ok , _acc ->
192- { :halt , true }
193-
194- :retry , _acc ->
195- if System . monotonic_time ( :second ) >= deadline ,
196- do: { :halt , false } ,
197- else: { :cont , false }
198- end )
118+ _ ->
119+ nil
120+ end
199121 end
200122
201- defp port_open? ( port ) do
202- case :gen_tcp . connect ( ~c" localhost" , port , [ ] , 1000 ) do
203- { :ok , socket } ->
204- :gen_tcp . close ( socket )
205- true
206-
207- { :error , _ } ->
208- false
123+ # Minimal JSON extractor for `bootstrap_servers`: avoids adding a JSON dep just for tests.
124+ defp decode_bootstrap ( json ) do
125+ case Regex . run ( ~r/ "bootstrap_servers"\s *:\s *"([^"]+)"/ , json ) do
126+ [ _ , servers ] -> { :ok , servers }
127+ _ -> { :error , "no bootstrap_servers in: #{ json } " }
209128 end
210129 end
211130end
0 commit comments