private Task <SnippetResult> DoStuffAndReturnWhenIdFinished(Guid snippetId) { // Use a TCS for a Task without a "thread" // http://blogs.msdn.com/b/pfxteam/archive/2009/06/02/9685804.aspx var tcs = new TaskCompletionSource <SnippetResult>(); try { // Post in queue, and return immediately // The pool/deque will "call us back" (set the status) when finished var snippetData = repository.Get(snippetId); var snippetInfo = new SnippetInfo() { assemblyFile = snippetData.AssemblyBytes, mainTypeName = SnippetData.SnippetTypeName, methodName = SnippetData.SnippetMethodName, submissionId = Guid.NewGuid().ToString() }; domainPool.SubmitSnippet(snippetInfo, tcs); } catch (Exception ex) { tcs.TrySetException(ex); } // Return immediately, with the task we can await on return(tcs.Task); }
internal void SubmitSnippet(SnippetInfo snippetInfo, TaskCompletionSource <SnippetResult> completion) { // check if we need to re-introduce some domains in the pool if (Interlocked.Read(ref numberOfThreadsInPool) < NumberOfDomainsInPool) { // A sort of "double-check optimization": we enter here only if there is a shortage of threads/domains, but // we actually create one only if there is a slot int emptySlotIdx = -1; lock (poolLock) { // find the empty slot for (int i = 0; i < NumberOfDomainsInPool; ++i) { if (poolDomains[i] == null) { poolDomains[i] = new PooledDomainData(); emptySlotIdx = i; } } } if (emptySlotIdx >= 0) { var thread = CreateDomainThread(emptySlotIdx); thread.Start(); } } // Store the continuation _before_ adding the snippet info the the processing queue if (!String.IsNullOrEmpty(snippetInfo.submissionId) && completion != null) { submissionMap[snippetInfo.submissionId] = completion; } snippetsQueue.Add(snippetInfo); }
internal void SubmitSnippet(SnippetInfo snippetInfo) { SubmitSnippet(snippetInfo, null); }
private Thread CreateDomainThread(int threadIndex) { System.Diagnostics.Debug.WriteLine("CreateDomainThread: " + threadIndex); var myPoolDomain = poolDomains[threadIndex]; var thread = new Thread(() => { Interlocked.Increment(ref numberOfThreadsInPool); // Here we enforce the "one domain, one thread" relationship try { var appDomain = AppDomainHelpers.CreateSandbox("Host Sandbox"); var manager = (SimpleHostAppDomainManager)appDomain.DomainManager; lock (poolLock) { myPoolDomain.domainId = appDomain.Id; } while (!snippetsQueue.IsCompleted) { defaultDomainManager.ResetContextFor(myPoolDomain); // And here is were we "rent" one AppDomain and use it to run a snippet SnippetInfo snippetToRun = null; try { snippetToRun = snippetsQueue.Take(); } catch (InvalidOperationException) { // Someone called "complete". No more snippets in this process. // We want to exit the pool. return; } if (snippetToRun != null) { System.Diagnostics.Debug.WriteLine("Starting snippet '" + snippetToRun.methodName + "' in domain " + myPoolDomain.domainId); SnippetResult result = new SnippetResult(); bool recycleDomain = false; try { Interlocked.Increment(ref myPoolDomain.numberOfUsages); // Record when we started long startTimestamp = StopwatchExtensions.GetTimestampMillis(); System.Diagnostics.Debug.WriteLine("Starting execution at " + startTimestamp); Thread.VolatileWrite(ref myPoolDomain.timeOfSubmission, startTimestamp); // Thread transitions into the AppDomain // This function DOES NOT throw result = manager.InternalRun(appDomain, snippetToRun.assemblyFile, snippetToRun.mainTypeName, snippetToRun.methodName, true); // ...back to the main AppDomain Debug.Assert(AppDomain.CurrentDomain.IsDefaultAppDomain()); long currentTime = StopwatchExtensions.GetTimestampMillis(); result.executionTime = currentTime - Thread.VolatileRead(ref myPoolDomain.timeOfSubmission); // Flag it as "not executing" Thread.VolatileWrite(ref myPoolDomain.timeOfSubmission, 0); } catch (ThreadAbortException ex) { // Someone called abort on us. result.exception = ex.Message; // It may be possible to use this domain again, otherwise we will recycle if (Object.Equals(ex.ExceptionState, timeoutAbortToken)) { // The abort was issued by us because we timed out System.Diagnostics.Debug.WriteLine("Thread Abort due to timeout"); result.status = SnippetStatus.Timeout; } else if (Object.Equals(ex.ExceptionState, threadsExaustedAbortToken)) { System.Diagnostics.Debug.WriteLine("Thread Abort due to thread exaustion"); result.status = SnippetStatus.ResourceError; } else { // If it wasn't us, give us time to record the result; we will recycle (unload) the domain System.Diagnostics.Debug.WriteLine("Thread Abort due to external factors"); result.status = SnippetStatus.CriticalError; recycleDomain = true; } // Give us time to record the result; if needed, we will recycle (unload) the domain later Thread.ResetAbort(); } catch (Exception ex) { result.exception = ex.Message; // Check if someone is misbehaving, throwing something else to "mask" the TAE if (Thread.CurrentThread.ThreadState == System.Threading.ThreadState.AbortRequested) { result.status = SnippetStatus.CriticalError; recycleDomain = true; // Give us time to record the result; we will recycle (unload) the domain Thread.ResetAbort(); } else { result.status = SnippetStatus.ExecutionError; } } // "Although C# only allows you to throw objects of type Exception and types deriving from it, // other languages don’t have any such restriction." // http://weblogs.asp.net/kennykerr/introduction-to-msil-part-5-exception-handling // It turns out this is no longer necessary // http://blogs.msdn.com/b/jmanning/archive/2005/09/16/469091.aspx // catch { } // No need to catch StackOverflowException; the Host will escalate to a (rude) domain unload // for us // TODO: check that AppDomain.DomainUnload is called anyway! // Before looping, check if we are OK; we reuse the domain only if we are not leaking int threadsInDomain = defaultDomainManager.GetThreadCount(appDomain.Id); int memoryUsage = defaultDomainManager.GetMemoryUsage(appDomain.Id); System.Diagnostics.Debug.WriteLine("============= AppDomain {0} =============", appDomain.Id); System.Diagnostics.Debug.WriteLine("Finished in: {0}", result.executionTime); System.Diagnostics.Debug.WriteLine("Status: {0}", result.status); if (result.exception != null) { System.Diagnostics.Debug.WriteLine("Exception: " + result.exception); } System.Diagnostics.Debug.WriteLine("Threads: {0}", threadsInDomain); System.Diagnostics.Debug.WriteLine("Memory: {0}", memoryUsage); System.Diagnostics.Debug.WriteLine("========================================"); if (threadsInDomain > 1) { // The snippet is leaking threads // Flag snippet as "leaking" result.status = SnippetStatus.CriticalError; System.Diagnostics.Debug.WriteLine("Leaking snippet"); recycleDomain = true; } else if (MaxReuse > 0 && myPoolDomain.numberOfUsages >= MaxReuse) { System.Diagnostics.Debug.WriteLine("Domain too old"); // Same if the domain is too old recycleDomain = true; } // Return the result to the caller if (!String.IsNullOrEmpty(snippetToRun.submissionId)) { var completion = submissionMap[snippetToRun.submissionId]; // TCS is thread-safe (can be called cross-thread) completion.TrySetResult(result); } if (recycleDomain) { System.Diagnostics.Debug.WriteLine("Recycling domain..."); RecycleDomain(threadIndex); } else { // Otherwise, ensure that what was allocated by the snippet is freed GC.Collect(); System.Diagnostics.Debug.WriteLine("MonitoringSurvivedMemorySize {0}", appDomain.MonitoringSurvivedMemorySize); System.Diagnostics.Debug.WriteLine("MonitoringTotalAllocatedMemorySize {0}", appDomain.MonitoringTotalAllocatedMemorySize); System.Diagnostics.Debug.WriteLine("MonitoringTotalProcessorTime {0}", appDomain.MonitoringTotalProcessorTime); } } } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("Exception caught:\n" + ex.ToString()); RecycleDomain(threadIndex); } }); thread.Name = "DomainPool thread " + threadIndex; myPoolDomain.mainThread = thread; thread.Priority = ThreadPriority.BelowNormal; return(thread); }