public void Timeout() { var policy = new LinearRetryPolicy(TransientDetector, retryInterval: TimeSpan.FromSeconds(0.5), timeout: TimeSpan.FromSeconds(1.5)); var times = new List <DateTime>(); Assert.Equal(int.MaxValue, policy.MaxAttempts); Assert.Equal(TimeSpan.FromSeconds(0.5), policy.RetryInterval); Assert.Equal(TimeSpan.FromSeconds(1.5), policy.Timeout); Assert.Throws <TransientException>( () => { policy.Invoke( () => { times.Add(DateTime.UtcNow); throw new TransientException(); }); }); Assert.Equal(4, times.Count); // Additional test to verify this serious problem is fixed: // // https://github.com/nforgeio/neonKUBE/issues/762 // // We'll wait a bit longer to enure that any (incorrect) deadline computed // by the policy when constructed above does not impact a subsequent run. Thread.Sleep(TimeSpan.FromSeconds(4)); times.Clear(); Assert.Equal(TimeSpan.FromSeconds(0.5), policy.RetryInterval); Assert.Equal(TimeSpan.FromSeconds(1.5), policy.Timeout); Assert.Throws <TransientException>( () => { policy.Invoke( () => { times.Add(DateTime.UtcNow); throw new TransientException(); }); }); Assert.Equal(4, times.Count); }
/// <summary> /// Initializes the database and establishes the connections. /// </summary> private void Initialize() { // Establish the database connections. Note that rather than using a fragile // warmup delay, we'll just retry establishing the connections for up to 15 seconds. // // Note that we're going to delete the Cassandra keyspace and Postgres database // before recreating them so they'll start out empty for each unit test. var retry = new LinearRetryPolicy(e => true, int.MaxValue, retryInterval: TimeSpan.FromSeconds(1), timeout: new TimeSpan(15)); // Establish the Cassandra session, recreating the keyspace. var cluster = Cluster.Builder() .AddContactPoint("localhost") .WithPort(ycqlPort) .Build(); retry.Invoke( () => { CassandraSession = cluster.Connect(); }); CassandraSession.Execute($"DROP KEYSPACE IF EXISTS \"{cassandraKeyspace}\""); CassandraSession.Execute($"CREATE KEYSPACE \"{cassandraKeyspace}\""); CassandraSession = cluster.Connect(cassandraKeyspace); // Establish the Postgres connection, recreating the database. PostgresConnection = new NpgsqlConnection($"host=localhost;port={ysqlPort};user id=yugabyte;password="******"DROP DATABASE IF EXISTS \"{postgresDatabase}\"", PostgresConnection); command.ExecuteNonQuery(); command = new NpgsqlCommand($"CREATE DATABASE \"{postgresDatabase}\"", PostgresConnection); command.ExecuteNonQuery(); PostgresConnection = new NpgsqlConnection($"host=localhost;database={postgresDatabase};port={ysqlPort};user id=yugabyte;password="); PostgresConnection.Open(); }
public void FailDelayed_Result() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); Assert.Throws <NotImplementedException>( () => { policy.Invoke <string>( () => { times.Add(DateTime.UtcNow); if (times.Count < 2) { throw new TransientException(); } else { throw new NotImplementedException(); } }); }); Assert.Equal(2, times.Count); VerifyIntervals(times, policy); }
public void SuccessCustom_Result() { var policy = new LinearRetryPolicy(TransientDetector, maxAttempts: 4, retryInterval: TimeSpan.FromSeconds(2)); var times = new List <DateTime>(); Assert.Equal(4, policy.MaxAttempts); Assert.Equal(TimeSpan.FromSeconds(2), policy.RetryInterval); var success = policy.Invoke( () => { times.Add(DateTime.UtcNow); if (times.Count < policy.MaxAttempts) { throw new TransientException(); } return("WOOHOO!"); }); Assert.Equal("WOOHOO!", success); Assert.Equal(policy.MaxAttempts, times.Count); VerifyIntervals(times, policy); }
public void SuccessDelayedAggregateArray() { var policy = new LinearRetryPolicy(new Type[] { typeof(NotReadyException), typeof(KeyNotFoundException) }); var times = new List <DateTime>(); var success = false; policy.Invoke( () => { times.Add(DateTime.UtcNow); if (times.Count < policy.MaxAttempts) { if (times.Count % 1 == 0) { throw new AggregateException(new NotReadyException()); } else { throw new AggregateException(new KeyNotFoundException()); } } success = true; }); Assert.True(success); Assert.Equal(policy.MaxAttempts, times.Count); VerifyIntervals(times, policy); }
/// <summary> /// Used to start the fixture within a <see cref="ComposedFixture"/>. /// </summary> /// <param name="image"> /// Optionally specifies the NATS container image. This defaults to /// <b>ghcr.io/neonrelease/nats:latest</b> or <b>ghcr.io/ghcr.io/neonrelease-dev/nats:latest</b> depending /// on whether the assembly was built from a git release branch or not. /// </param> /// <param name="name">Optionally specifies the container name (defaults to <c>nats-test</c>).</param> /// <param name="args">Optional NATS server command line arguments.</param> /// <param name="hostInterface"> /// Optionally specifies the host interface where the container public ports will be /// published. This defaults to <see cref="ContainerFixture.DefaultHostInterface"/> /// but may be customized. This needs to be an IPv4 address. /// </param> public void StartAsComposed( string image = null, string name = "nats-test", string[] args = null, string hostInterface = null) { image = image ?? $"{NeonHelper.NeonLibraryBranchRegistry}/nats:latest"; this.hostInterface = hostInterface; base.CheckWithinAction(); var dockerArgs = new string[] { "--detach", "-p", $"{GetHostInterface(hostInterface)}:4222:4222", "-p", $"{GetHostInterface(hostInterface)}:8222:8222", "-p", $"{GetHostInterface(hostInterface)}:6222:6222" }; if (!IsRunning) { StartAsComposed(name, image, dockerArgs, args); } var factory = new ConnectionFactory(); var retry = new LinearRetryPolicy(exception => true, 20, TimeSpan.FromSeconds(0.5)); retry.Invoke( () => { Connection = factory.CreateConnection($"nats://{GetHostInterface(hostInterface, forConnection: true)}:4222"); }); }
/// <summary> /// Establishes the server connection. /// </summary> private void Connect() { var factory = new StanConnectionFactory(); var retry = new LinearRetryPolicy(exception => true, 20, TimeSpan.FromSeconds(0.5)); retry.Invoke( () => { Connection = factory.CreateConnection("test-cluster", nameof(NatsStreamingFixture)); }); }
public void SuccessImmediate_Result() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); var success = policy.Invoke( () => { times.Add(DateTime.UtcNow); return("WOOHOO!"); }); Assert.Single(times); Assert.Equal("WOOHOO!", success); }
public void SuccessImmediate() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); var success = false; policy.Invoke( () => { times.Add(DateTime.UtcNow); success = true; }); Assert.Single(times); Assert.True(success); }
public void FailImmediate_Result() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); Assert.Throws <NotImplementedException>( () => { policy.Invoke <string>( () => { times.Add(DateTime.UtcNow); throw new NotImplementedException(); }); }); Assert.Single(times); }
public void FailAll_Result() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); Assert.Throws <TransientException>( () => { policy.Invoke <string>( () => { times.Add(DateTime.UtcNow); throw new TransientException(); }); }); Assert.Equal(policy.MaxAttempts, times.Count); VerifyIntervals(times, policy); }
/// <summary> /// Restarts the NATS container to clear any previous state and returns the /// new client connection. /// </summary> /// <returns>The new connection.</returns> public new IConnection Restart() { base.Restart(); if (Connection != null) { Connection.Dispose(); Connection = null; } var factory = new ConnectionFactory(); var retry = new LinearRetryPolicy(exception => true, 20, TimeSpan.FromSeconds(0.5)); retry.Invoke( () => { Connection = factory.CreateConnection($"nats://{GetHostInterface(hostInterface, forConnection: true)}:4222"); }); return(Connection); }
public void SuccessDelayed_Result() { var policy = new LinearRetryPolicy(TransientDetector); var times = new List <DateTime>(); var success = policy.Invoke( () => { times.Add(DateTime.UtcNow); if (times.Count < policy.MaxAttempts) { throw new TransientException(); } return("WOOHOO!"); }); Assert.Equal("WOOHOO!", success); Assert.Equal(policy.MaxAttempts, times.Count); VerifyIntervals(times, policy); }
public void SuccessDelayedByType() { var policy = new LinearRetryPolicy(typeof(NotReadyException)); var times = new List <DateTime>(); var success = false; policy.Invoke( () => { times.Add(DateTime.UtcNow); if (times.Count < policy.MaxAttempts) { throw new NotReadyException(); } success = true; }); Assert.True(success); Assert.Equal(policy.MaxAttempts, times.Count); VerifyIntervals(times, policy); }
/// <summary> /// Removes a specific fixture section from the <b>hosts</b> file or all /// fixture sections if <paramref name="fixtureId"/> is <c>null</c>. /// </summary> /// <param name="fixtureId"> /// Identifies the fixture section to be removed or <c>null</c> to /// remove all fixture sections. /// </param> private static void RemoveSection(string fixtureId = null) { var sb = new StringBuilder(); var changed = false; var sectionGuids = new HashSet <string>(); // Update the [hosts] file. retryFile.Invoke( () => { if (File.Exists(HostsPath)) { using (var reader = new StreamReader(new FileStream(HostsPath, FileMode.Open, FileAccess.ReadWrite))) { var guid = fixtureId ?? string.Empty; var startMarker = $"# START-NEON-HOSTS-FIXTURE-{guid}"; var endMarker = $"# END-NEON-HOSTS-FIXTURE-{guid}"; var inSection = false; foreach (var line in reader.Lines()) { if (inSection) { if (line.StartsWith(endMarker)) { inSection = false; changed = true; } } else { if (line.StartsWith(startMarker)) { // Extract the section GUID from the marker because we'll need // these below when we verify that the resolver has picked up // the changes. var posGuid = line.LastIndexOf('-') + 1; var sectionGuid = line.Substring(posGuid); if (!sectionGuids.Contains(sectionGuid)) { sectionGuids.Add(sectionGuid); } inSection = true; changed = true; } else { if (!inSection) { sb.AppendLine(line); } } } } } } if (changed) { File.WriteAllText(HostsPath, sb.ToString()); } }); if (changed) { // We need to verify that the local DNS resolver has picked up the change // by verifying that none of the removed section hostnames resolve. retryReady.Invoke( () => { foreach (var sectionGuid in sectionGuids) { var hostname = GetSectionHostname(sectionGuid); var addresses = GetHostAddresses(hostname); if (addresses.Length > 0) { throw new NotReadyException($"Waiting for [{hostname}] to be removed by the local DNS resolver."); } } }); } }
/// <summary> /// <para> /// Used to temporarily modify the <b>hosts</b> file used by the DNS resolver /// for testing, debugging or other purposes. /// </para> /// <note> /// <b>WARNING:</b> Modifying the <b>hosts</b> file will impact all processes /// on the system, not just the current one and this is designed to be used by /// a single process at a time. /// </note> /// </summary> /// <param name="hostEntries">A dictionary mapping the hostnames to an IP address or <c>null</c>.</param> /// <param name="section"> /// <para> /// Optionally specifies the string to use to mark the hostnames section. This /// defaults to <b>MODIFY</b> which will delimit the section with <b># NEON-BEGIN-MODIFY</b> /// and <b># NEON-END-MODIFY</b>. You may pass a different string to identify a custom section. /// </para> /// <note> /// The string passed must be a valid DNS hostname label that must begin with a letter /// followed by letters, digits or dashes. The maximum length is 63 characters. /// </note> /// </param> /// <remarks> /// <note> /// This method requires elevated administrative privileges. /// </note> /// <para> /// This method adds or removes a temporary section of host entry definitions /// delimited by special comment lines. When <paramref name="hostEntries"/> is /// non-null and non-empty, the section will be added or updated. Otherwise, the /// section will be removed. /// </para> /// <para> /// You can remove all host sections by passing both <paramref name="hostEntries"/> /// and <paramref name="section"/> as <c>null</c>. /// </para> /// </remarks> public static void ModifyLocalHosts(Dictionary <string, IPAddress> hostEntries = null, string section = "MODIFY") { #if XAMARIN throw new NotSupportedException(); #else if (hostEntries != null && string.IsNullOrWhiteSpace(section)) { throw new ArgumentNullException(nameof(section)); } if (section != null) { var sectionOK = char.IsLetter(section[0]) && section.Length <= 63; if (sectionOK) { foreach (var ch in section) { if (!char.IsLetterOrDigit(ch) && ch != '-') { sectionOK = false; break; } } } if (!sectionOK) { throw new ArgumentException("Suffix is not a valid DNS host name label.", nameof(section)); } section = section.ToUpperInvariant(); } string hostsPath; if (NeonHelper.IsWindows) { hostsPath = Path.Combine(Environment.GetEnvironmentVariable("windir"), "System32", "drivers", "etc", "hosts"); } else if (NeonHelper.IsLinux || NeonHelper.IsOSX) { hostsPath = "/etc/hosts"; } else { throw new NotSupportedException(); } // We're seeing transient file locked errors when trying to update the [hosts] file. // My guess is that this is cause by the Window DNS resolver opening the file as // READ/WRITE to prevent it from being modified while the resolver is reading any // changes. // // We're going to mitigate this by retrying a few times. // // It can take a bit of time for the Windows DNS resolver to pick up the change. // // https://github.com/nforgeio/neonKUBE/issues/244 // // We're going to mitigate this by writing a [neon-modify-local-hosts.nhive.io] record with // a random IP address and then wait for for the DNS resolver to report the correct address. // // Note that this only works on Windows and perhaps OSX. This doesn't work on // Linux because there's no central DNS resolver there. See the issue below for // more information: // // https://github.com/nforgeio/neonKUBE/issues/271 var updateHost = section != null ? $"{section.ToLowerInvariant()}.neonforge-marker" : $"H-{Guid.NewGuid().ToString("d")}.neonforge-marker"; var updateAddress = GetRandomAddress(); var lines = new List <string>(); var existingHosts = new Dictionary <string, string>(StringComparer.InvariantCultureIgnoreCase); var different = false; retryFile.Invoke( () => { var beginMarker = $"# NEON-BEGIN-"; var endMarker = $"# NEON-END-"; if (section != null) { beginMarker += section; endMarker += section; } var inputLines = File.ReadAllLines(hostsPath); var inSection = false; // Load lines of text from the current [hosts] file, without // any lines for the named section. We're going to parse those // lines instead, so we can compare them against the [hostEntries] // passed to determine whether we actually need to update the // [hosts] file. lines.Clear(); existingHosts.Clear(); foreach (var line in inputLines) { var trimmed = line.Trim(); if (trimmed == beginMarker || (section == null && trimmed.StartsWith(beginMarker))) { inSection = true; } else if (trimmed == endMarker || (section == null && trimmed.StartsWith(endMarker))) { inSection = false; } else { if (inSection) { // The line is within the named section, so we're going to parse // the host entry (if any) and add it to [existingHosts]. if (trimmed.Length == 0 || trimmed.StartsWith("#")) { // Ignore empty or comment lines (just to be safe). continue; } // We're going to simply assume that the address and hostname // are separated by whitespace and that there's no other junk // on the line (like comments added by the operator). If there // is any junk, we'll capture that too and then the entries // won't match and we'll just end up rewriting the section // (which is reasonable). // // Note that we're going to ignore the special marker entry. var fields = line.Split(new char[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); var address = fields[0]; var hostname = fields.Length > 1 ? fields[1] : string.Empty; if (!hostname.EndsWith(".neonforge-marker")) { existingHosts[hostname] = address; } } else { // The line is not in the named section, so we'll // include it as as. lines.Add(line); } } } // Compare the existing entries against the new ones and rewrite // the [hosts] file only if they are different. if (hostEntries != null && hostEntries.Count == existingHosts.Count) { foreach (var item in hostEntries) { if (!existingHosts.TryGetValue(item.Key, out var existingAddress) || item.Value.ToString() != existingAddress) { different = true; break; } } if (!different) { return; } } // Append the section if it has any host entries. if (hostEntries?.Count > 0) { lines.Add(beginMarker); // Append the special update host with a random IP address. var address = updateAddress.ToString(); lines.Add($" {address}{new string(' ', 16 - address.Length)} {updateHost}"); // Append the new entries. foreach (var item in hostEntries) { address = item.Value.ToString(); lines.Add($" {address}{new string(' ', 16 - address.Length)} {item.Key}"); } lines.Add(endMarker); } File.WriteAllLines(hostsPath, lines.ToArray()); }); if (!different) { // We didn't detect any changes to the section above so we're going to // exit without rewriting the [hosts] file. return; } if (NeonHelper.IsWindows) { // Flush the DNS cache (and I believe this reloads the [hosts] file too). var response = NeonHelper.ExecuteCapture("ipconfig", "/flushdns"); if (response.ExitCode != 0) { throw new ToolException($"ipconfig: [exitcode={response.ExitCode}]: {response.ErrorText}"); } } else if (NeonHelper.IsOSX) { // This should work on OS/X 12 (Sierra) or later. We're not going to support // older OS/X versions for now but here's some information on how to do this // if we change our minds: // // https://help.dreamhost.com/hc/en-us/articles/214981288-Flushing-your-DNS-cache-in-Mac-OS-X-and-Linux // // Note that this requires that the current process be running as ROOT. var response = NeonHelper.ExecuteCapture("killall", "-HUP mDNSResponder"); if (response.ExitCode != 0) { throw new ToolException($"killall -HUP mDNSResponder: [exitcode={response.ExitCode}]: {response.ErrorText}"); } response = NeonHelper.ExecuteCapture("killall", "mDNSResponderHelper"); if (response.ExitCode != 0) { throw new ToolException($"killall mDNSResponderHelper: [exitcode={response.ExitCode}]: {response.ErrorText}"); } response = NeonHelper.ExecuteCapture("dscacheutil", "-flushcache"); if (response.ExitCode != 0) { throw new ToolException($"dscacheutil -flushcache [exitcode={response.ExitCode}]: {response.ErrorText}"); } } else if (NeonHelper.IsLinux) { // Linux distributions don't typically enable a system-wide DNS cache so we're // not going to worry about this here. } if (NeonHelper.IsWindows || NeonHelper.IsOSX) { // Poll the local DNS resolver until it reports the correct address for the // [neon-modify-local-hosts.nhive.io]. // // If [hostEntries] is not null and contains at least one entry, we'll lookup // [neon-modify-local-hosts.neon] and compare the IP address to ensure that the // resolver has loaded the new entries. // // If [hostEntries] is null or empty, we'll wait until there are no records // for [neon-modify-local-hosts.neon] to ensure that the resolver has reloaded // the hosts file after we removed the entries. // // Note that we're going to count the retries and after the 20th (about 2 second's // worth of 100ms polling), we're going to rewrite the [hosts] file. I've seen // situations where at appears that the DNS resolver isn't re-reading [hosts] // after it's been updated. I believe this is due to the file being written // twice, once to remove the section and then shortly again there after to // write the section again. I believe there's a chance that the resolver may // miss the second file change notification. Writing the file again should // trigger a new notification. var retryCount = 0; retryReady.Invoke( () => { var addresses = GetHostAddresses(updateHost); if (hostEntries?.Count > 0) { // Ensure that the new records have been loaded by the resolver. if (addresses.Length != 1) { RewriteOn20thRetry(hostsPath, lines, ref retryCount); throw new NotReadyException($"[{updateHost}] lookup is returning [{addresses.Length}] results. There should be [1]."); } if (addresses[0].ToString() != updateAddress.ToString()) { RewriteOn20thRetry(hostsPath, lines, ref retryCount); throw new NotReadyException($"DNS is [{updateHost}={addresses[0]}] rather than [{updateAddress}]."); } } else { // Ensure that the resolver recognizes that we removed the records. if (addresses.Length != 0) { RewriteOn20thRetry(hostsPath, lines, ref retryCount); throw new NotReadyException($"[{updateHost}] lookup is returning [{addresses.Length}] results. There should be [0]."); } } }); } // $hack(jefflill): Wait a bit longer just to be safe. Thread.Sleep(TimeSpan.FromSeconds(2)); #endif }
/// <summary> /// Used to start the fixture within a <see cref="ComposedFixture"/>. /// </summary> /// <param name="settings">Optional Cadence settings.</param> /// <param name="name">Optionally specifies the Cadence container name (defaults to <c>cadence-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="defaultDomain">Optionally specifies the default domain for the fixture's client. This defaults to <b>test-domain</b>.</param> /// <param name="logLevel">Specifies the Cadence log level. This defaults to <see cref="Neon.Diagnostics.LogLevel.None"/>.</param> /// <param name="reconnect"> /// Optionally specifies that a new Cadence 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 case. /// </param> /// <param name="keepRunning"> /// Optionally indicates that the container 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="CadenceClient.Reset()"/> to /// put the Cadence client library into its initial state before the fixture starts as well /// as when the fixture itself is reset. /// </param> /// <remarks> /// <note> /// A fresh Cadence 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( CadenceSettings settings = null, string name = "cadence-dev", string composeFile = DefaultComposeFile, string defaultDomain = DefaultDomain, 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) { // $hack(jefflill): // // The [temporal-dev] Docker test stack may be running from a previous test run. // We need to stop this to avoid network port conflicts. We're just going to // force the removal of the stack's Docker containers. // // This is somewhat fragile because it hardcodes the container names and won't // remove any other stack assets like networks. NeonHelper.ExecuteCapture(NeonHelper.DockerCli, new object[] { "rm", "--force", new string[] { "temporal-dev_cassandra_1", "temporal-dev_temporal-web_1", "temporal-dev_temporal_1" } }); // Reset CadenceClient to its initial state. this.noReset = noReset; if (!noReset) { CadenceClient.Reset(); } // Start the Cadence container. base.StartAsComposed(name, composeFile, keepRunning); // It can take Cadence server some time to start. Rather than relying on [cadence-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.Cadence); socket.Close(); }); Thread.Sleep(TimeSpan.FromSeconds(5)); // Wait a bit longer for luck! // Initialize the settings. settings = settings ?? new CadenceSettings() { CreateDomain = true, DefaultDomain = defaultDomain, LogLevel = logLevel }; if (settings.Servers.Count == 0) { settings.Servers.Add($"http://localhost:{NetworkPorts.Cadence}"); } this.settings = settings; this.reconnect = reconnect; if (!noClient) { // Establish the Cadence connection via the cadence proxy. Client = CadenceClient.ConnectAsync(settings).Result; HttpClient = new HttpClient() { BaseAddress = Client.ListenUri }; } } }
/// <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 }; } } }