/// <summary> /// Used to start the fixture within a <see cref="ComposedFixture"/>. /// </summary> /// <param name="settings">Optional Temporal settings.</param> /// <param name="name">Optionally specifies the Docker compose application name (defaults to <c>temporal-dev</c>).</param> /// <param name="composeFile"> /// <para> /// Optionally specifies the Temporal Docker compose file text. This defaults to /// <see cref="DefaultComposeFile"/> which configures Temporal server to start with /// a new Cassandra database instance listening on port <b>9042</b> as well as the /// Temporal web UI running on port <b>8088</b>. Temporal server is listening on /// its standard gRPC port <b>7233</b>. /// </para> /// <para> /// You may specify your own Docker compose text file to customize this by configuring /// a different backend database, etc. /// </para> /// </param> /// <param name="defaultNamespace">Optionally specifies the default namespace for the fixture's client. This defaults to <b>test-namespace</b>.</param> /// <param name="logLevel">Specifies the Temporal log level. This defaults to <see cref="Neon.Diagnostics.LogLevel.None"/>.</param> /// <param name="reconnect"> /// Optionally specifies that a new Temporal connection <b>should</b> be established for each /// unit test case. By default, the same connection will be reused which will save about a /// second per test. /// </param> /// <param name="keepRunning"> /// Optionally indicates that the compose application should remain running after the fixture is disposed. /// This is handy for using the Temporal web UI for port mortems after tests have completed. /// </param> /// <param name="noClient"> /// Optionally disables establishing a client connection when <c>true</c> /// is passed. The <see cref="Client"/> and <see cref="HttpClient"/> properties /// will be set to <c>null</c> in this case. /// </param> /// <param name="noReset"> /// Optionally prevents the fixture from calling <see cref="TemporalClient.Reset()"/> to /// put the Temporal client library into its initial state before the fixture starts as well /// as when the fixture itself is reset. /// </param> /// <remarks> /// <note> /// A fresh Temporal client <see cref="Client"/> will be established every time this /// fixture is started, regardless of whether the fixture has already been started. This /// ensures that each unit test will start with a client in the default state. /// </note> /// </remarks> public void StartAsComposed( TemporalSettings settings = null, string name = "temporal-dev", string composeFile = DefaultComposeFile, string defaultNamespace = Namespace, LogLevel logLevel = LogLevel.None, bool reconnect = false, bool keepRunning = false, bool noClient = false, bool noReset = false) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(composeFile), nameof(composeFile)); base.CheckWithinAction(); if (!IsRunning) { // The [cadence-dev] container started by [CadenceFixture] has conflicting ports // for Cassandra and the web UI, so we're going to stop that container if it's running. NeonHelper.Execute(NeonHelper.DockerCli, new object[] { "rm", "--force", new string[] { "cadence-dev_cadence_1", "cadence-dev_cadence-web_1", "cadence-dev_cassandra_1", "cadence-dev_statsd_1" } }); // Reset TemporalClient to its initial state. this.noReset = noReset; if (!noReset) { TemporalClient.Reset(); } // Start the Temporal Docker compose application. base.StartAsComposed(name, composeFile, keepRunning); // It can take Temporal server some time to start. Rather than relying on [temporal-proxy] // to handle retries (which may take longer than the connect timeout), we're going to wait // up to 4 minutes for Temporal to start listening on its RPC socket. var retry = new LinearRetryPolicy(e => true, maxAttempts: int.MaxValue, retryInterval: TimeSpan.FromSeconds(0.5), timeout: TimeSpan.FromMinutes(4)); retry.Invoke( () => { // The [socket.Connect()] calls below will throw [SocketException] until // Temporal starts listening on its RPC socket. var socket = new Socket(AddressFamily.InterNetworkV6, SocketType.Stream, ProtocolType.Tcp); socket.Connect(IPAddress.IPv6Loopback, NetworkPorts.Temporal); socket.Close(); }); Thread.Sleep(TimeSpan.FromSeconds(5)); // Wait a bit longer for luck! // Initialize the settings. settings = settings ?? new TemporalSettings() { HostPort = $"localhost:{NetworkPorts.Temporal}", CreateNamespace = true, Namespace = defaultNamespace, ProxyLogLevel = logLevel, }; this.settings = settings; this.reconnect = reconnect; if (!noClient) { // Establish the Temporal connection. Client = TemporalClient.ConnectAsync(settings).Result; HttpClient = new HttpClient() { BaseAddress = Client.ListenUri }; } } }