1 #region Copyright notice and license
2 
3 // Copyright 2015 gRPC authors.
4 //
5 // Licensed under the Apache License, Version 2.0 (the "License");
6 // you may not use this file except in compliance with the License.
7 // You may obtain a copy of the License at
8 //
9 //     http://www.apache.org/licenses/LICENSE-2.0
10 //
11 // Unless required by applicable law or agreed to in writing, software
12 // distributed under the License is distributed on an "AS IS" BASIS,
13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 // See the License for the specific language governing permissions and
15 // limitations under the License.
16 
17 #endregion
18 
19 using System;
20 using System.Collections.Generic;
21 using System.Linq;
22 using System.Runtime.InteropServices;
23 using System.Threading.Tasks;
24 using Grpc.Core.Internal;
25 using Grpc.Core.Logging;
26 using Grpc.Core.Utils;
27 
28 namespace Grpc.Core
29 {
30     /// <summary>
31     /// Encapsulates initialization and shutdown of gRPC library.
32     /// </summary>
33     public class GrpcEnvironment
34     {
35         const int MinDefaultThreadPoolSize = 4;
36         const int DefaultBatchContextPoolSharedCapacity = 10000;
37         const int DefaultBatchContextPoolThreadLocalCapacity = 64;
38         const int DefaultRequestCallContextPoolSharedCapacity = 10000;
39         const int DefaultRequestCallContextPoolThreadLocalCapacity = 64;
40 
41         static object staticLock = new object();
42         static GrpcEnvironment instance;
43         static int refCount;
44         static int? customThreadPoolSize;
45         static int? customCompletionQueueCount;
46         static bool inlineHandlers;
47         static int batchContextPoolSharedCapacity = DefaultBatchContextPoolSharedCapacity;
48         static int batchContextPoolThreadLocalCapacity = DefaultBatchContextPoolThreadLocalCapacity;
49         static int requestCallContextPoolSharedCapacity = DefaultRequestCallContextPoolSharedCapacity;
50         static int requestCallContextPoolThreadLocalCapacity = DefaultRequestCallContextPoolThreadLocalCapacity;
51         static readonly HashSet<Channel> registeredChannels = new HashSet<Channel>();
52         static readonly HashSet<Server> registeredServers = new HashSet<Server>();
53         static readonly AtomicCounter nativeInitCounter = new AtomicCounter();
54 
55         static ILogger logger = new LogLevelFilterLogger(new ConsoleLogger(), LogLevel.Off, true);
56 
57         readonly IObjectPool<BatchContextSafeHandle> batchContextPool;
58         readonly IObjectPool<RequestCallContextSafeHandle> requestCallContextPool;
59         readonly GrpcThreadPool threadPool;
60         readonly DebugStats debugStats = new DebugStats();
61         readonly AtomicCounter cqPickerCounter = new AtomicCounter();
62 
63         bool isShutdown;
64 
65         /// <summary>
66         /// Returns a reference-counted instance of initialized gRPC environment.
67         /// Subsequent invocations return the same instance unless reference count has dropped to zero previously.
68         /// </summary>
AddRef()69         internal static GrpcEnvironment AddRef()
70         {
71             ShutdownHooks.Register();
72 
73             lock (staticLock)
74             {
75                 refCount++;
76                 if (instance == null)
77                 {
78                     instance = new GrpcEnvironment();
79                 }
80                 return instance;
81             }
82         }
83 
84         /// <summary>
85         /// Decrements the reference count for currently active environment and asynchronously shuts down the gRPC environment if reference count drops to zero.
86         /// </summary>
ReleaseAsync()87         internal static async Task ReleaseAsync()
88         {
89             GrpcEnvironment instanceToShutdown = null;
90             lock (staticLock)
91             {
92                 GrpcPreconditions.CheckState(refCount > 0);
93                 refCount--;
94                 if (refCount == 0)
95                 {
96                     instanceToShutdown = instance;
97                     instance = null;
98                 }
99             }
100 
101             if (instanceToShutdown != null)
102             {
103                 await instanceToShutdown.ShutdownAsync().ConfigureAwait(false);
104             }
105         }
106 
GetRefCount()107         internal static int GetRefCount()
108         {
109             lock (staticLock)
110             {
111                 return refCount;
112             }
113         }
114 
RegisterChannel(Channel channel)115         internal static void RegisterChannel(Channel channel)
116         {
117             lock (staticLock)
118             {
119                 GrpcPreconditions.CheckNotNull(channel);
120                 registeredChannels.Add(channel);
121             }
122         }
123 
UnregisterChannel(Channel channel)124         internal static void UnregisterChannel(Channel channel)
125         {
126             lock (staticLock)
127             {
128                 GrpcPreconditions.CheckNotNull(channel);
129                 GrpcPreconditions.CheckArgument(registeredChannels.Remove(channel), "Channel not found in the registered channels set.");
130             }
131         }
132 
RegisterServer(Server server)133         internal static void RegisterServer(Server server)
134         {
135             lock (staticLock)
136             {
137                 GrpcPreconditions.CheckNotNull(server);
138                 registeredServers.Add(server);
139             }
140         }
141 
UnregisterServer(Server server)142         internal static void UnregisterServer(Server server)
143         {
144             lock (staticLock)
145             {
146                 GrpcPreconditions.CheckNotNull(server);
147                 GrpcPreconditions.CheckArgument(registeredServers.Remove(server), "Server not found in the registered servers set.");
148             }
149         }
150 
151         /// <summary>
152         /// Requests shutdown of all channels created by the current process.
153         /// </summary>
ShutdownChannelsAsync()154         public static Task ShutdownChannelsAsync()
155         {
156             HashSet<Channel> snapshot = null;
157             lock (staticLock)
158             {
159                 snapshot = new HashSet<Channel>(registeredChannels);
160             }
161             return Task.WhenAll(snapshot.Select((channel) => channel.ShutdownAsync()));
162         }
163 
164         /// <summary>
165         /// Requests immediate shutdown of all servers created by the current process.
166         /// </summary>
KillServersAsync()167         public static Task KillServersAsync()
168         {
169             HashSet<Server> snapshot = null;
170             lock (staticLock)
171             {
172                 snapshot = new HashSet<Server>(registeredServers);
173             }
174             return Task.WhenAll(snapshot.Select((server) => server.KillAsync()));
175         }
176 
177         /// <summary>
178         /// Gets application-wide logger used by gRPC.
179         /// </summary>
180         /// <value>The logger.</value>
181         public static ILogger Logger
182         {
183             get
184             {
185                 return logger;
186             }
187         }
188 
189         /// <summary>
190         /// Sets the application-wide logger that should be used by gRPC.
191         /// </summary>
SetLogger(ILogger customLogger)192         public static void SetLogger(ILogger customLogger)
193         {
194             GrpcPreconditions.CheckNotNull(customLogger, "customLogger");
195             logger = customLogger;
196         }
197 
198         /// <summary>
199         /// Sets the number of threads in the gRPC thread pool that polls for internal RPC events.
200         /// Can be only invoked before the <c>GrpcEnviroment</c> is started and cannot be changed afterwards.
201         /// Setting thread pool size is an advanced setting and you should only use it if you know what you are doing.
202         /// Most users should rely on the default value provided by gRPC library.
203         /// Note: this method is part of an experimental API that can change or be removed without any prior notice.
204         /// </summary>
SetThreadPoolSize(int threadCount)205         public static void SetThreadPoolSize(int threadCount)
206         {
207             lock (staticLock)
208             {
209                 GrpcPreconditions.CheckState(instance == null, "Can only be set before GrpcEnvironment is initialized");
210                 GrpcPreconditions.CheckArgument(threadCount > 0, "threadCount needs to be a positive number");
211                 customThreadPoolSize = threadCount;
212             }
213         }
214 
215         /// <summary>
216         /// Sets the number of completion queues in the  gRPC thread pool that polls for internal RPC events.
217         /// Can be only invoked before the <c>GrpcEnviroment</c> is started and cannot be changed afterwards.
218         /// Setting the number of completions queues is an advanced setting and you should only use it if you know what you are doing.
219         /// Most users should rely on the default value provided by gRPC library.
220         /// Note: this method is part of an experimental API that can change or be removed without any prior notice.
221         /// </summary>
SetCompletionQueueCount(int completionQueueCount)222         public static void SetCompletionQueueCount(int completionQueueCount)
223         {
224             lock (staticLock)
225             {
226                 GrpcPreconditions.CheckState(instance == null, "Can only be set before GrpcEnvironment is initialized");
227                 GrpcPreconditions.CheckArgument(completionQueueCount > 0, "threadCount needs to be a positive number");
228                 customCompletionQueueCount = completionQueueCount;
229             }
230         }
231 
232         /// <summary>
233         /// By default, gRPC's internal event handlers get offloaded to .NET default thread pool thread (<c>inlineHandlers=false</c>).
234         /// Setting <c>inlineHandlers</c> to <c>true</c> will allow scheduling the event handlers directly to
235         /// <c>GrpcThreadPool</c> internal threads. That can lead to significant performance gains in some situations,
236         /// but requires user to never block in async code (incorrectly written code can easily lead to deadlocks).
237         /// Inlining handlers is an advanced setting and you should only use it if you know what you are doing.
238         /// Most users should rely on the default value provided by gRPC library.
239         /// Note: this method is part of an experimental API that can change or be removed without any prior notice.
240         /// Note: <c>inlineHandlers=true</c> was the default in gRPC C# v1.4.x and earlier.
241         /// </summary>
SetHandlerInlining(bool inlineHandlers)242         public static void SetHandlerInlining(bool inlineHandlers)
243         {
244             lock (staticLock)
245             {
246                 GrpcPreconditions.CheckState(instance == null, "Can only be set before GrpcEnvironment is initialized");
247                 GrpcEnvironment.inlineHandlers = inlineHandlers;
248             }
249         }
250 
251         /// <summary>
252         /// Sets the parameters for a pool that caches batch context instances. Reusing batch context instances
253         /// instead of creating a new one for every C core operation helps reducing the GC pressure.
254         /// Can be only invoked before the <c>GrpcEnviroment</c> is started and cannot be changed afterwards.
255         /// This is an advanced setting and you should only use it if you know what you are doing.
256         /// Most users should rely on the default value provided by gRPC library.
257         /// Note: this method is part of an experimental API that can change or be removed without any prior notice.
258         /// </summary>
SetBatchContextPoolParams(int sharedCapacity, int threadLocalCapacity)259         public static void SetBatchContextPoolParams(int sharedCapacity, int threadLocalCapacity)
260         {
261             lock (staticLock)
262             {
263                 GrpcPreconditions.CheckState(instance == null, "Can only be set before GrpcEnvironment is initialized");
264                 GrpcPreconditions.CheckArgument(sharedCapacity >= 0, "Shared capacity needs to be a non-negative number");
265                 GrpcPreconditions.CheckArgument(threadLocalCapacity >= 0, "Thread local capacity needs to be a non-negative number");
266                 batchContextPoolSharedCapacity = sharedCapacity;
267                 batchContextPoolThreadLocalCapacity = threadLocalCapacity;
268             }
269         }
270 
271         /// <summary>
272         /// Sets the parameters for a pool that caches request call context instances. Reusing request call context instances
273         /// instead of creating a new one for every requested call in C core helps reducing the GC pressure.
274         /// Can be only invoked before the <c>GrpcEnviroment</c> is started and cannot be changed afterwards.
275         /// This is an advanced setting and you should only use it if you know what you are doing.
276         /// Most users should rely on the default value provided by gRPC library.
277         /// Note: this method is part of an experimental API that can change or be removed without any prior notice.
278         /// </summary>
SetRequestCallContextPoolParams(int sharedCapacity, int threadLocalCapacity)279         public static void SetRequestCallContextPoolParams(int sharedCapacity, int threadLocalCapacity)
280         {
281             lock (staticLock)
282             {
283                 GrpcPreconditions.CheckState(instance == null, "Can only be set before GrpcEnvironment is initialized");
284                 GrpcPreconditions.CheckArgument(sharedCapacity >= 0, "Shared capacity needs to be a non-negative number");
285                 GrpcPreconditions.CheckArgument(threadLocalCapacity >= 0, "Thread local capacity needs to be a non-negative number");
286                 requestCallContextPoolSharedCapacity = sharedCapacity;
287                 requestCallContextPoolThreadLocalCapacity = threadLocalCapacity;
288             }
289         }
290 
291         /// <summary>
292         /// Occurs when <c>GrpcEnvironment</c> is about the start the shutdown logic.
293         /// If <c>GrpcEnvironment</c> is later initialized and shutdown, the event will be fired again (unless unregistered first).
294         /// </summary>
295         public static event EventHandler ShuttingDown;
296 
297         /// <summary>
298         /// Creates gRPC environment.
299         /// </summary>
GrpcEnvironment()300         private GrpcEnvironment()
301         {
302             GrpcNativeInit();
303             batchContextPool = new DefaultObjectPool<BatchContextSafeHandle>(() => BatchContextSafeHandle.Create(), batchContextPoolSharedCapacity, batchContextPoolThreadLocalCapacity);
304             requestCallContextPool = new DefaultObjectPool<RequestCallContextSafeHandle>(() => RequestCallContextSafeHandle.Create(), requestCallContextPoolSharedCapacity, requestCallContextPoolThreadLocalCapacity);
305             threadPool = new GrpcThreadPool(this, GetThreadPoolSizeOrDefault(), GetCompletionQueueCountOrDefault(), inlineHandlers);
306             threadPool.Start();
307         }
308 
309         /// <summary>
310         /// Gets the completion queues used by this gRPC environment.
311         /// </summary>
312         internal IReadOnlyCollection<CompletionQueueSafeHandle> CompletionQueues
313         {
314             get
315             {
316                 return this.threadPool.CompletionQueues;
317             }
318         }
319 
320         internal IObjectPool<BatchContextSafeHandle> BatchContextPool => batchContextPool;
321 
322         internal IObjectPool<RequestCallContextSafeHandle> RequestCallContextPool => requestCallContextPool;
323 
324         internal bool IsAlive
325         {
326             get
327             {
328                 return this.threadPool.IsAlive;
329             }
330         }
331 
332         /// <summary>
333         /// Picks a completion queue in a round-robin fashion.
334         /// Shouldn't be invoked on a per-call basis (used at per-channel basis).
335         /// </summary>
PickCompletionQueue()336         internal CompletionQueueSafeHandle PickCompletionQueue()
337         {
338             var cqIndex = (int) ((cqPickerCounter.Increment() - 1) % this.threadPool.CompletionQueues.Count);
339             return this.threadPool.CompletionQueues.ElementAt(cqIndex);
340         }
341 
342         /// <summary>
343         /// Gets the completion queue used by this gRPC environment.
344         /// </summary>
345         internal DebugStats DebugStats
346         {
347             get
348             {
349                 return this.debugStats;
350             }
351         }
352 
353         /// <summary>
354         /// Gets version of gRPC C core.
355         /// </summary>
GetCoreVersionString()356         internal static string GetCoreVersionString()
357         {
358             var ptr = NativeMethods.Get().grpcsharp_version_string();  // the pointer is not owned
359             return Marshal.PtrToStringAnsi(ptr);
360         }
361 
GrpcNativeInit()362         internal static void GrpcNativeInit()
363         {
364             if (!IsNativeShutdownAllowed && nativeInitCounter.Count > 0)
365             {
366                 // Normally grpc_init and grpc_shutdown calls should come in pairs (C core does reference counting),
367                 // but in case we avoid grpc_shutdown calls altogether, calling grpc_init has no effect
368                 // besides incrementing an internal C core counter that could theoretically overflow.
369                 // To avoid this theoretical possibility we guard repeated calls to grpc_init()
370                 // with a 64-bit atomic counter (that can't realistically overflow).
371                 return;
372             }
373             NativeMethods.Get().grpcsharp_init();
374             nativeInitCounter.Increment();
375         }
376 
GrpcNativeShutdown()377         internal static void GrpcNativeShutdown()
378         {
379             if (IsNativeShutdownAllowed)
380             {
381                 NativeMethods.Get().grpcsharp_shutdown();
382             }
383         }
384 
385         /// <summary>
386         /// Shuts down this environment.
387         /// </summary>
ShutdownAsync()388         private async Task ShutdownAsync()
389         {
390             if (isShutdown)
391             {
392                 throw new InvalidOperationException("ShutdownAsync has already been called");
393             }
394 
395             await Task.Run(() => ShuttingDown?.Invoke(this, null)).ConfigureAwait(false);
396 
397             await threadPool.StopAsync().ConfigureAwait(false);
398             requestCallContextPool.Dispose();
399             batchContextPool.Dispose();
400             GrpcNativeShutdown();
401             isShutdown = true;
402 
403             debugStats.CheckOK();
404         }
405 
GetThreadPoolSizeOrDefault()406         private int GetThreadPoolSizeOrDefault()
407         {
408             if (customThreadPoolSize.HasValue)
409             {
410                 return customThreadPoolSize.Value;
411             }
412             // In systems with many cores, use half of the cores for GrpcThreadPool
413             // and the other half for .NET thread pool. This heuristic definitely needs
414             // more work, but seems to work reasonably well for a start.
415             return Math.Max(MinDefaultThreadPoolSize, Environment.ProcessorCount / 2);
416         }
417 
GetCompletionQueueCountOrDefault()418         private int GetCompletionQueueCountOrDefault()
419         {
420             if (customCompletionQueueCount.HasValue)
421             {
422                 return customCompletionQueueCount.Value;
423             }
424             // by default, create a completion queue for each thread
425             return GetThreadPoolSizeOrDefault();
426         }
427 
428         // On some platforms (specifically iOS), thread local variables in native code
429         // require initialization/destruction. By skipping the grpc_shutdown() call,
430         // we avoid a potential crash where grpc_shutdown() has already destroyed
431         // the thread local variables, but some C core's *_destroy() methods still
432         // need to run (e.g. they may be run by finalizer thread which is out of our control)
433         // For more context, see https://github.com/grpc/grpc/issues/16294
434         private static bool IsNativeShutdownAllowed => !PlatformApis.IsXamarinIOS && !PlatformApis.IsUnityIOS;
435 
436         private static class ShutdownHooks
437         {
438             static object staticLock = new object();
439             static bool hooksRegistered;
440 
Register()441             public static void Register()
442             {
443                 lock (staticLock)
444                 {
445                     if (!hooksRegistered)
446                     {
447                         // Under normal circumstances, the user is expected to shutdown all
448                         // the gRPC channels and servers before the application exits. The following
449                         // hooks provide some extra handling for cases when this is not the case,
450                         // in the effort to achieve a reasonable behavior on shutdown.
451 #if NETSTANDARD
452                         // No action required at shutdown on .NET Core
453                         // - In-progress P/Invoke calls (such as grpc_completion_queue_next) don't seem
454                         //   to prevent a .NET core application from terminating, so no special handling
455                         //   is needed.
456                         // - .NET core doesn't run finalizers on shutdown, so there's no risk of getting
457                         //   a crash because grpc_*_destroy methods for native objects being invoked
458                         //   in wrong order.
459                         // TODO(jtattermusch): Verify that the shutdown hooks are still not needed
460                         // once we add support for new platforms using netstandard (e.g. Xamarin).
461 #else
462                         // On desktop .NET framework and Mono, we need to register for a shutdown
463                         // event to explicitly shutdown the GrpcEnvironment.
464                         // - On Desktop .NET framework, we need to do a proper shutdown to prevent a crash
465                         //   when the framework attempts to run the finalizers for SafeHandle object representing the native
466                         //   grpc objects. The finalizers calls the native grpc_*_destroy methods (e.g. grpc_server_destroy)
467                         //   in a random order, which is not supported by gRPC.
468                         // - On Mono, the process would freeze as the GrpcThreadPool threads are sleeping
469                         //   in grpc_completion_queue_next P/Invoke invocation and mono won't let the
470                         //   process shutdown until the P/Invoke calls return. We achieve that by shutting down
471                         //   the completion queue(s) which associated with the GrpcThreadPool, which will
472                         //   cause the grpc_completion_queue_next calls to return immediately.
473                         AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => { HandleShutdown(); };
474                         AppDomain.CurrentDomain.DomainUnload += (sender, eventArgs) => { HandleShutdown(); };
475 #endif
476                     }
477                     hooksRegistered = true;
478                 }
479             }
480 
481             /// <summary>
482             /// Handler for AppDomain.DomainUnload, AppDomain.ProcessExit and AssemblyLoadContext.Unloading hooks.
483             /// </summary>
HandleShutdown()484             private static void HandleShutdown()
485             {
486                 Task.WaitAll(GrpcEnvironment.ShutdownChannelsAsync(), GrpcEnvironment.KillServersAsync());
487             }
488         }
489     }
490 }
491