Beispiel #1
0
        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);
        }
Beispiel #2
0
        /// <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();
        }
Beispiel #3
0
        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);
        }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
        /// <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");
            });
        }
Beispiel #7
0
        /// <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));
            });
        }
Beispiel #8
0
        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);
        }
Beispiel #9
0
        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);
        }
Beispiel #10
0
        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);
        }
Beispiel #11
0
        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);
        }
Beispiel #12
0
        /// <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);
        }
Beispiel #13
0
        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);
        }
Beispiel #14
0
        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);
        }
Beispiel #15
0
        /// <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.");
                        }
                    }
                });
            }
        }
Beispiel #16
0
        /// <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
        }
Beispiel #17
0
        /// <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
                    };
                }
            }
        }
Beispiel #18
0
        /// <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
                    };
                }
            }
        }