/// <summary>Start the Network Thread, using the specified API version level</summary> /// <param name="apiVersion">API version that will be used by this application. A value of 0 mean "max safe default version".</param> /// <remarks>This method can only be called once per process, and the API version cannot be changed until the process restarts.</remarks> public static void Start(int apiVersion) { if (s_started) { return; } //BUGBUG: Specs say we cannot restart the network thread anymore in the process after stopping it ! :( s_started = true; apiVersion = CheckApiVersion(apiVersion); if (Logging.On) { Logging.Info(typeof(Fdb), "Start", $"Selecting fdb API version {apiVersion}"); } FdbError err = FdbNative.SelectApiVersion(apiVersion); if (err != FdbError.Success) { if (Logging.On) { Logging.Error(typeof(Fdb), "Start", $"Failed to fdb API version {apiVersion}: {err}"); } switch (err) { case FdbError.ApiVersionNotSupported: { // bad version was selected ? // note: we already bound check the values before, so that means that fdb_c.dll is either an older version or an incompatible new version. throw new FdbException(err, $"The API version {apiVersion} is not supported by the FoundationDB client library (fdb_c.dll) installed on this system. The binding only supports versions {GetMinApiVersion()} to {GetMaxApiVersion()}. You either need to upgrade the .NET binding or the FoundationDB client library to a newer version."); } #if DEBUG case FdbError.ApiVersionAlreadySet: { // Temporary hack to allow multiple debugging using the cached host process in VS Console.Error.WriteLine("FATAL: CANNOT REUSE EXISTING PROCESS! FoundationDB client cannot be restarted once stopped. Current process will be terminated."); Environment.FailFast("FATAL: CANNOT REUSE EXISTING PROCESS! FoundationDB client cannot be restarted once stopped. Current process will be terminated."); break; } #endif } DieOnError(err); } s_apiVersion = apiVersion; if (!string.IsNullOrWhiteSpace(Fdb.Options.TracePath)) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will trace client activity in '{Fdb.Options.TracePath}'"); } // create trace directory if missing... if (!SystemIO.Directory.Exists(Fdb.Options.TracePath)) { SystemIO.Directory.CreateDirectory(Fdb.Options.TracePath); } DieOnError(SetNetworkOption(FdbNetworkOption.TraceEnable, Fdb.Options.TracePath)); } #pragma warning disable 618 if (!string.IsNullOrWhiteSpace(Fdb.Options.TLSPlugin)) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will use custom TLS plugin '{Fdb.Options.TLSPlugin}'"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSPlugin, Fdb.Options.TLSPlugin)); } #pragma warning restore 618 if (Fdb.Options.TLSCertificateBytes.IsPresent) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will load TLS root certificate and private key from memory ({Fdb.Options.TLSCertificateBytes.Count} bytes)"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSCertBytes, Fdb.Options.TLSCertificateBytes)); } else if (!string.IsNullOrWhiteSpace(Fdb.Options.TLSCertificatePath)) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will load TLS root certificate and private key from '{Fdb.Options.TLSCertificatePath}'"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSCertPath, Fdb.Options.TLSCertificatePath)); } if (Fdb.Options.TLSPrivateKeyBytes.IsPresent) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will load TLS private key from memory ({Fdb.Options.TLSPrivateKeyBytes.Count} bytes)"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSKeyBytes, Fdb.Options.TLSPrivateKeyBytes)); } else if (!string.IsNullOrWhiteSpace(Fdb.Options.TLSPrivateKeyPath)) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will load TLS private key from '{Fdb.Options.TLSPrivateKeyPath}'"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSKeyPath, Fdb.Options.TLSPrivateKeyPath)); } if (Fdb.Options.TLSVerificationPattern.IsPresent) { if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", $"Will verify TLS peers with pattern '{Fdb.Options.TLSVerificationPattern}'"); } DieOnError(SetNetworkOption(FdbNetworkOption.TLSVerifyPeers, Fdb.Options.TLSVerificationPattern)); } try { } finally { // register with the AppDomain to ensure that everything is cleared when the process exists s_appDomainUnloadHandler = (sender, args) => { if (s_started) { //note: since app domain is unloading, the logger may also have already stopped... if (Logging.On) { Logging.Verbose(typeof(Fdb), "AppDomainUnloadHandler", "AppDomain is unloading, stopping FoundationDB Network Thread..."); } Stop(); } }; AppDomain.CurrentDomain.DomainUnload += s_appDomainUnloadHandler; AppDomain.CurrentDomain.ProcessExit += s_appDomainUnloadHandler; if (Logging.On) { Logging.Verbose(typeof(Fdb), "Start", "Setting up Network Thread..."); } DieOnError(FdbNative.SetupNetwork()); s_started = true; //BUGBUG: already set at the start of the method. Maybe we need state flags ? } if (Logging.On) { Logging.Info(typeof(Fdb), "Start", "Network thread has been set up"); } StartEventLoop(); }
private static void EventLoop() { //TODO: we need to move the crash handling logic outside this method, so that an app can hook up an event and device what to do: crash or keep running (dangerous!). // At least, the app would get the just to do some emergency shutdown logic before letting the process die.... try { s_eventLoopRunning = true; s_eventLoopThreadId = Thread.CurrentThread.ManagedThreadId; if (Logging.On) { Logging.Verbose(typeof(Fdb), "EventLoop", $"FDB Event Loop running on thread #{Fdb.s_eventLoopThreadId.Value}..."); } var err = FdbNative.RunNetwork(); if (err != FdbError.Success) { if (s_eventLoopStopRequested || Environment.HasShutdownStarted) { // this was requested, or can be explained by the computer shutting down... if (Logging.On) { Logging.Info(typeof(Fdb), "EventLoop", $"The fdb network thread returned with error code {err}: {GetErrorMessage(err)}"); } } else { // this was NOT expected ! if (Logging.On) { Logging.Error(typeof(Fdb), "EventLoop", $"The fdb network thread returned with error code {err}: {GetErrorMessage(err)}"); } #if DEBUG Console.Error.WriteLine("THE FDB NETWORK EVENT LOOP HAS FAILED!"); Console.Error.WriteLine("=> " + err); // REVIEW: should we FailFast in release mode also? // => this may be a bit surprising for most users when applications unexpectedly crash for for no apparent reason. Environment.FailFast("The FoundationDB Network Event Loop failed with error " + err + " and was terminated."); #endif } } } catch (Exception e) { if (e is ThreadAbortException) { // some other thread tried to Abort() us. This probably means that we should exit ASAP... Thread.ResetAbort(); return; } //note: any error is this thread is BAD NEWS for the process, the the network thread usually cannot be restarted safely. if (e is AccessViolationException) { // An access violation occured inside the native code. This good be caused by: // - a bug in fdb_c.dll // - a bug in our own marshalling code that calls into fdb_c.dll // - some other random heap corruption that caused us to pass bogus data to fdb_c.dll // - a random cosmic ray that flipped some bits in memory... if (Debugger.IsAttached) { Debugger.Break(); } // This error is VERY BAD NEWS, and means that we CANNOT continue safely running fdb in this process // The only reasonable option is to exit the process immediately ! Console.Error.WriteLine("THE FDB NETWORK EVENT LOOP HAS CRASHED!"); Console.Error.WriteLine("=> " + e.ToString()); Environment.FailFast("The FoundationDB Network Event Loop crashed with an Access Violation, and had to be terminated. You may try to create full memory dumps, as well as attach a debugger to this process (it will automatically break when this problem occurs).", e); return; } if (Logging.On) { Logging.Exception(typeof(Fdb), "EventLoop", e); } #if DEBUG // if we are running in DEBUG build, we want to get the attention of the developer on this. // the best way is to make the test runner explode in mid-air with a scary looking message! Console.Error.WriteLine("THE FDB NETWORK EVENT LOOP HAS CRASHED!"); Console.Error.WriteLine("=> " + e.ToString()); // REVIEW: should we FailFast in release mode also? // => this may be a bit surprising for most users when applications unexpectedly crash for for no apparent reason. Environment.FailFast("The FoundationDB Network Event Loop crashed and had to be terminated: " + e.Message, e); #endif } finally { if (Logging.On) { Logging.Verbose(typeof(Fdb), "EventLoop", "FDB Event Loop stopped"); } s_eventLoopThreadId = null; s_eventLoopRunning = false; } }
/// <summary>Return the error message matching the specified error code</summary> public static string GetErrorMessage(FdbError code) { return(FdbNative.GetError(code)); }
/// <summary>Stops the thread running the FDB event loop</summary> private static void StopEventLoop() { if (s_eventLoopStarted) { // We cannot be called from the network thread itself, or else we will dead lock ! Fdb.EnsureNotOnNetworkThread(); if (Logging.On) { Logging.Verbose(typeof(Fdb), "StopEventLoop", "Stopping network thread..."); } s_eventLoopStopRequested = true; var err = FdbNative.StopNetwork(); if (err != FdbError.Success) { if (Logging.On) { Logging.Warning(typeof(Fdb), "StopEventLoop", $"Failed to stop event loop: {err.ToString()}"); } } s_eventLoopStarted = false; var thread = s_eventLoop; if (thread != null && thread.IsAlive) { // BUGBUG: specs says that we need to wait for the network thread to stop gracefully, or else data integrity may not be guaranteed... // We should wait for a bit, and only attempt to Abort() the thread after a timeout (30sec ? more ?) // keep track of how much time it took to stop... var duration = Stopwatch.StartNew(); try { //TODO: replace with a ManualResetEvent that would get signaled at the end of the event loop ? while (thread.IsAlive && duration.Elapsed.TotalSeconds < 5) { // wait a bit... Thread.Sleep(250); } if (thread.IsAlive) { if (Logging.On) { Logging.Warning(typeof(Fdb), "StopEventLoop", $"The fdb network thread has not stopped after {duration.Elapsed.TotalSeconds:N0} seconds. Forcing shutdown..."); } // Force a shutdown thread.Abort(); bool stopped = thread.Join(TimeSpan.FromSeconds(30)); //REVIEW: is this even useful? If the thread is stuck in a native P/Invoke call, it won't get notified until it returns to managed code ... // => in that case, we have a zombie thread on our hands... if (!stopped) { if (Logging.On) { Logging.Warning(typeof(Fdb), "StopEventLoop", $"The fdb network thread failed to stop after more than {duration.Elapsed.TotalSeconds:N0} seconds. Transaction integrity may not be guaranteed."); } } } } catch (ThreadAbortException) { // Should not happen, unless we are called from a thread that is itself being stopped ? } finally { s_eventLoop = null; duration.Stop(); if (duration.Elapsed.TotalSeconds >= 20) { if (Logging.On) { Logging.Warning(typeof(Fdb), "StopEventLoop", $"The fdb network thread took a long time to stop ({duration.Elapsed.TotalSeconds:N0} seconds)."); } } } } } }
/// <summary>Returns the maximum API version currently supported by the installed client.</summary> /// <remarks>The version of the installed client (fdb_c.dll) can be different higher (or lower) than the version supported by this binding (FoundationDB.Client.dll)! /// If you want the highest possible version that is supported by both the binding and the client, you must call <see cref="GetMaxSafeApiVersion()"/>. /// Attempts to select an API version higher than this value will fail. /// </remarks> public static int GetMaxApiVersion() { return(FdbNative.GetMaxApiVersion()); }