Single instance WCF service with concurrent tasks (that can be limited) - c#

I am trying to build a WCF service that -
Is single instance
Allows clients to make multiple request to functions (eg. StartJob)
StarJob(request) 'queues' the request to the TaskFactory (one instance) running on Concurrent task schedule (implemented as per example
As tasks in the task factory are completed, the response is returned
While a task is running and more requests come in, they get queued (provide max concurrent number is reached)
Objective is to build a system that accepts requests from clients and queues them for processing.
Currently, my code (shown below), runs all requests simultaneously without taking the max concurrent number of task scheduler into account.
Questions
What am I missing out?
Any good example/reference I can look at? (I am sure this is not an uncommon use case)
Code
IService
[ServiceContract]
public interface ISupportService
{
[OperationContract]
Task<TaskResponse> StartTask(TaskRequest taskRequest);
}
Service
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class SupportService : ISupportService
{
private static TaskRequestHandler taskRequestHandler;
public SupportService()
{
taskRequestHandler = TaskRequestHandler.GetInstance();
}
public Task<TaskResponse> StartTask(TaskRequest taskRequest)
{
var tcs = new TaskCompletionSource<TaskResponse>();
if (!IsTaskRequestValid(taskRequest))
tcs.SetResult(new TaskResponse()});
taskRequestHandler.StartTaskAsync(taskRequest, lockHandler).ContinueWith(task => { tcs.SetResult(task.Result); });
return tcs.Task;
}
}
TaskRequestHandler
public class TaskRequestHandler
{
private ConcurrentTaskScheduler taskScheduler;
private TaskFactory taskFactory;
private TaskRequestHandler()
{
taskScheduler = new ConcurrentTaskScheduler(2);
taskFactory = new TaskFactory(taskScheduler);
}
private Task<TaskResponse> StartTaskAsync (TaskRequest request, LockHandler lockHandler)
{
var tcs = new TaskCompletionSource<TaskResponse>();
taskFactory.StartNew(() =>
{
//Some task with tcs.SetResults()
});
return tcs.Task;
}
}

Aaaah! A big miss on my part. The action executed in taskFactory was completing before I expected it to. As such, all the tasks appeared to be running in parallel.
I updated the action code to monitor the action completion correctly and raising correct callbacks, the above code worked fine.
However, made a minor change -
There is not need for StartTask(TaskRequest taskRequest) to return a Task. Rather, just returning the TaskResponse will suffice (as WCF takes care of Async and Sync functionality of every OperationContract)

Related

Run an async method only once, and return the same result to all concurrent and future calls [duplicate]

This question already has answers here:
Enforce an async method to be called once
(4 answers)
Closed 6 months ago.
I'm writing an ASP.net Core 6 application (but the question is more about C# in general) where I have a controller action like this:
[HttpGet]
public async Task<IActionResult> MyAction() {
var result = await myService.LongOperationAsync();
return Ok(result);
}
Basically, the action calls a service that needs to do some complex operation and can take a bit of time to respond, up to a minute. Obviously, if in the meantime another request arrives a second run of LongOperationAsync() starts, consuming even more resources.
What I would like to do is redesign this so that the calls to LongOperationAsync() don't run in parallel, but instead wait for the result of the first call and then all return the same result, so that LongOperationAsync() only runs once.
I thought of a few ways to implement this (for example by using a scheduler like Quartz to run the call and then check if a relevant Job is already running before enqueueing another one) but they all require quite a bit of relatively complicated plumbing.
So I guess my questions are:
Is there an established design pattern / best practice to implement this scenario? Is it even practical / a good idea?
Are there features in the C# language and/or the ASP.net Core framework that facilitate implementing something like this?
Clarification: basically I want to run the long-running operation only once, and "recycle" the result to any other call that was waiting without executing the long-running operation again.
You could use an async version of Lazy<T> to do this.
Stephen Toub has posted a sample implementation of LazyAsync<T> here, which I reproduce below:
public class AsyncLazy<T> : Lazy<Task<T>>
{
public AsyncLazy(Func<T> valueFactory) :
base(() => Task.Run(valueFactory))
{ }
public AsyncLazy(Func<Task<T>> taskFactory) :
base(() => Task.Run(taskFactory))
{ }
public TaskAwaiter<T> GetAwaiter() { return Value.GetAwaiter(); }
}
You could use it like this:
public class Program
{
public static async Task Main()
{
var test = new Test();
var task1 = Task.Run(async () => await test.AsyncString());
var task2 = Task.Run(async () => await test.AsyncString());
var task3 = Task.Run(async () => await test.AsyncString());
var results = await Task.WhenAll(task1, task2, task3);
Console.WriteLine(string.Join(", ", results));
}
}
public sealed class Test
{
public async Task<string> AsyncString()
{
Console.WriteLine("Started awaiting lazy string.");
var result = await _lazyString;
Console.WriteLine("Finished awaiting lazy string.");
return result;
}
static async Task<string> longRunningOperation()
{
Console.WriteLine("longRunningOperation() started.");
await Task.Delay(4000);
Console.WriteLine("longRunningOperation() finished.");
return "finished";
}
readonly AsyncLazy<string> _lazyString = new (longRunningOperation);
}
If you run this console app, you'll see that longRunningOperation() is only called once, and when it's finished all the tasks waiting on it will complete.
Try it on DotNetFiddle
As Matthew's answer points out, what you're looking for is an "async lazy". There is no built-in type for this, but it's not that hard to create.
What you should be aware of, though, is that there are a few design tradeoffs in an async lazy type:
What context the factory function is run on (the first invoker's context or no context at all). In ASP.NET Core, there isn't a context. So the Task.Factory.StartNew in Stephen Toub's example code is unnecessary overhead.
Whether failures should be cached. In the simple AsyncLazy<T> approach, if the factory function fails, then a faulted task is cached indefinitely.
When to reset. Again, by default the simple AsyncLazy<T> code never resets; a successful response is also cached indefinitely.
I'm assuming you do want the code to run multiple times; you just want it not to run multiple times concurrently. In that case, you want the async lazy to be reset immediately upon completion, whether successful or failed.
The resetting can be tricky. You want to reset only when it's completed, and only once (i.e., you don't want your reset code to clear the next operation). My go-to for this kind of logic is a unique identifier; I like to use new object() for this.
So, I would start with the Lazy<Task<T>> idea, but wrap it instead of derive, which allows you to do a reset, as such:
public class AsyncLazy<T>
{
private readonly Func<Task<T>> _factory;
private readonly object _mutex = new();
private Lazy<Task<T>> _lazy;
private object _id;
public AsyncLazy(Func<Task<T>> factory)
{
_factory = factory;
_lazy = new(_factory);
_id = new();
}
private (object LocalId, Task<T> Task) Start()
{
lock (_mutex)
{
return (_id, _lazy.Value);
}
}
private void Reset(object localId)
{
lock (_mutex)
{
if (localId != _id)
return;
_lazy = new(_factory);
_id = new();
}
}
public async Task<T> InvokeAsync()
{
var (localId, task) = Start();
try
{
return await task;
}
finally
{
Reset(localId);
}
}
}

.NET client-side WCF with queued requests

Background
I'm working on updating legacy software library. The legacy code uses an infinitely looping System.Threading.Thread that executes processes in the queue. These processes perform multiple requests with another legacy system that can only process one request at a time.
I'm trying to modernize, but I'm new to WCF services and there may be a big hole in my knowledge that'd simplify things.
WCF Client-Side Host
In modernizing, I'm trying to move to a client-side WCF service. The WCF service allows requests to be queued from multiple a applications. The service takes a request and returns a GUID back so that I can properly associate via the callbacks.
public class SomeService : ISomeService
{
public Guid AddToQueue(Request request)
{
// Code to add the request to a queue, return a Guid, etc.
}
}
public interface ISomeCallback
{
void NotifyExecuting(Guid guid)
void NotifyComplete(Guid guid)
void NotifyFault(Guid guid, byte[] data)
}
WCF Client Process Queues
The problem I'm having is that the legacy processes can include more than one request. Process 1 might do Request X then Request Y, and based on those results follow up with Request Z. With the legacy system, there might be Processes 1-10 queued up.
I have a cludgy model where the process is executed. I'm handling events on the process to know when it's finished or fails. But, it just feels really cludgy...
public class ActionsQueue
{
public IList<Action> PendingActions { get; private set; }
public Action CurrentAction { get; private set; }
public void Add(Action action)
{
PendingAction.Add(action)
if (CurrentAction is null)
ExecuteNextAction();
}
private void ExecuteNextAction()
{
if (PendingActions.Count > 0)
{
CurrentAction = PendingActions[0];
PendingActions.RemoveAt(0);
CurrentAction.Completed += OnActionCompleted;
CurrentAction.Execute();
}
}
private OnActionCompleted(object sender, EventArgs e)
{
CurrentAction = default;
ExecuteNextAction();
}
}
public class Action
{
internal void Execute()
{
// Instantiate the first request
// Add handlers to the first request
// Send it to the service
}
internal void OnRequestXComplete()
{
// Use the data that's come back from the request
// Proceed with future requests
}
}
With the client-side callback the GUID is matched up to the original request, and it raises a related event on the original requests. Again, the implementation here feels really cludgy.
I've seen example of Async methods for the host, having a Task returned, and then using an await on the Task. But, I've also seen recommendations not to do this.
Any recommendations on how to untangle this mess into something more usable are appreciated. Again, it's possible that there's a hole in my knowledge here that's keeping me from a better solutiong.
Thanks
Queued communication between the client and the server of WCF is usually possible using a NetMsmqbinding, which ensures persistent communication between the client and the server. See this article for specific examples.
If you need efficient and fast message processing, use a non-transactional queue and set the ExactlyOnce attribute to False, but this has a security impact. Check this docs for further info.
In case anyone comes along later with a similar issue, this is a rough sketch of what I ended up with:
[ServiceContract(Name="MyService", SessionMode=Session.Required]
public interface IMyServiceContract
{
[OperationContract()]
Task<string> ExecuteRequestAsync(Action action);
}
public class MyService: IMyServiceContract
{
private TaskQueue queue = new TaskQueue();
public async Task<string> ExecuteRequestAsync(Request request)
{
return await queue.Enqueue(() => request.Execute());
}
}
public class TaskQueue
{
private SemaphoreSlim semaphore;
public TaskQueue()
{
semaphore = new SemaphoreSlim(1);
}
Task<T> Enqueue<T>(Func<T> function)
{
await semaphore.WaitAsync();
try
{
return await Task.Factory.StartNew(() => function.invoke();)
}
finally
{
semaphore.Release();
}
}
}

Is there SignalR alternative with "return value to server" functionality?

My goal: Pass data do specific client who is connected to server and get results without calling Server method.
I tried use SignalR to do this (because It's very easy tool for me), but I can't get results (now I know why). I am working on ASP.NET Core 3.1.
My question: Is there SignalR alternative with "return value to server" functionality (call method with params on target client and get results)?
SignalR is usually used in a setup where there are multiple clients and a single server the clients connect to. This makes it a normal thing for clients to call the server and expect results back. Since the server usually does not really care about what individual clients are connected, and since the server usually broadcasts to a set of clients (e.g. using a group), the communication direction is mostly used for notifications or broadcasts. Single-target messages are possible but there isn’t a built-in mechanism for a request/response pattern.
In order to make this work with SignalR you will need to have a way for the client to call back the server. So you will need a hub action to send the response to.
That alone doesn’t make it difficult but what might do is that you will need to link a client-call with an incoming result message received by a hub. For that, you will have to build something.
Here’s an example implementation to get you starting. The MyRequestClient is a singleton service that basically encapsulates the messaging and offers you an asynchronous method that will call the client and only complete once the client responded by calling the callback method on the hub:
public class MyRequestClient
{
private readonly IHubContext<MyHub> _hubContext;
private ConcurrentDictionary<Guid, object> _pendingTasks = new ConcurrentDictionary<Guid, object>();
public MyRequestClient(IHubContext<MyHub> hubContext)
{
_hubContext = hubContext;
}
public async Task<int> Square(string connectionId, int number)
{
var requestId = Guid.NewGuid();
var source = new TaskCompletionSource<int>();
_pendingTasks[requestId] = source;
await _hubContext.Clients.Client(connectionId).SendAsync("Square", nameof(MyHub.SquareCallback), requestId, number);
return await source.Task;
}
public void SquareCallback(Guid requestId, int result)
{
if (_pendingTasks.TryRemove(requestId, out var obj) && obj is TaskCompletionSource<int> source)
source.SetResult(result);
}
}
In the hub, you then need the callback action to call the request client to complete the task:
public class MyHub : Hub
{
private readonly ILogger<MyHub> _logger;
private readonly MyRequestClient _requestClient;
public MyHub(ILogger<MyHub> logger, MyRequestClient requestClient)
{
_logger = logger;
_requestClient = requestClient;
}
public Task SquareCallback(Guid requestId, int number)
{
_requestClient.SquareCallback(requestId, number);
return Task.CompletedTask;
}
// just for demo purposes
public Task Start()
{
var connectionId = Context.ConnectionId;
_ = Task.Run(async () =>
{
var number = 42;
_logger.LogInformation("Starting Square: {Number}", number);
var result = await _requestClient.Square(connectionId, number);
_logger.LogInformation("Square returned: {Result}", result);
});
return Task.CompletedTask;
}
}
The Start hub action is only for demo purposes to have a way to start this with a valid connection id.
On the client, you then need to implement the client method and have it call the specified callback method once it’s done:
connection.on('Square', (callbackMethod, requestId, number) => {
const result = number * number;
connection.invoke(callbackMethod, requestId, result);
});
Finally, you can try this out by invoking the Start action by a client:
connection.invoke('Start');
Of course, this implementation is very basic and will need a few things like proper error handling and support for timing out the tasks if the client didn’t respond properly. It would also be possible to expand this to support arbitrary calls, without having you to create all these methods manually (e.g. by having a single callback method on the hub that is able to complete any task).

Ping replies of a WCF service while busy with long running operations

I'm developing a client/server application using WPF and WCF.
The server application hosts a WCF service that is in charge to execute clients requests and callback them when something occurs.
The service interface define a duplex callback contract with all OneWay operations.
(simplified) IService
[ServiceContract(CallbackContract = typeof(ISrvServiceCallback))]
public interface ISrvService
{
[OperationContract(IsOneWay = true)]
void Ping();
[OperationContract(IsOneWay = true)]
void LongRunningOperation();
}
public interface ISrvServiceCallback
{
[OperationContract(IsOneWay = true)]
void PingReply();
[OperationContract(IsOneWay = true)]
void LongRunningOperationStatus(string reply);
}
The service needs to mantain some objects that change states according to clients calls. For this reason I decided to have a singleton service.
(simplified) Service
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class SrvService : ISrvService
{
MyObject statusObject;
public void LongRunningOperation()
{
//calling back the client reporting operation status
OperationContext.Current.GetCallbackChannel<ISrvServiceCallback>()
.LongRunningOperationStatus("starting long running application");
statusObject.elaborateStatus();
//calling back the client reporting object status
OperationContext.Current.GetCallbackChannel<ISrvServiceCallback>()
.LongRunningOperationStatus("object status: " + statusObject.ToString());
}
public void Ping()
{
OperationContext.Current.GetCallbackChannel<ISrvServiceCallback>().PingReply();
}
public SrvService()
{
statusObject= ...statusObject init...
}
}
As you can see I have a Ping operation exposed by the service that a client calls (every 5 seconds) to check if the server application is available on the network (each client has a server connectivity icon with red=server not available, green=server not available).
When a client requests a long running operation, the server starts working on that operation and can't reply to the ping requests (the client's server connectivity icon turns red).
Once the long running operation finishes, the server replies to all the requests made by the client and the server connectivity icon turns back green).
I would like to find a way to develop the service so the server always replies to the ping requests, also when a long operation is running.
How can I do it considering that i need to keep
InstanceContextMode.Single to mantain the state of the objects of
the service?
Are there other/better ways to ping a WCF service
availability and visually display the result on the client?
With a singleton service you're going to need a multi threaded implementation of your server instance to get the desired behavior, at the very least you'll need to run LongRunningOperation on a separate thread. If this operation is inherently not thread safe, you'll need to guard against multiple concurrent calls to it specifically with a lock or semaphore, etc in the implementation. This way when a client calls LongRunningOperation(), it executes in a separate thread and is free to respond to ping requests.
There are many ways to implement this. By the way you worded your question the client seems to be making asynchronous calls (as it appears to be making ping requests while waiting for LongRunningOperation to return) - so I'm also going to assume you have some knowledge of asynchronous programming. WCF has some built in ways of handling concurrency, but most of the documentation does not cover singleton instances so you're going to need to read carefully and focus on that special case.
I've had the most success with the async/await pattern (see here and here) - Once this was set up properly I had a very reliable and predictable pattern for long running service calls in a stateful singleton service.
Also, as far as pings are concerned you do point out that you are simply displaying the connectivity status for the user, but if you had plans to use it for control (checking if the service is online before making a call) there is a lot of discussions here on why you should avoid it.
EDIT: Quick example with async/await
[ServiceContract]
public interface ISrvService()
{
[OperationContract]
bool Ping(); // doesnt need to be async
[OperationContract]
Task<string> LongRunningOperation();
}
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)]
public class SrvService : ISrvService
{
MyObject statusObject;
public async Task LongRunningOperation()
{
// lock/semaphore here if needed
await Task.Run(() => statusObject.elaborateStatus()); // you could impliment elaborateStatus() as an async Task method and call it without Task.Run
return statusObject.ToString();
}
public bool Ping()
{
return true;
}
public SrvService()
{
statusObject= ...statusObject init...
}
}
public class SrvClient : ClientBase<ISrvService>
{
public async Task<string> LongRunningOperation()
{
return await base.Channel.LongRunningOperation();
}
public async Task<bool> Ping()
{
// note that we still call this with an await. In the client we are awaiting the wcf service call
// this is independent of any async/await that happens on the server
return await Task.Run(() => base.Channel.Ping());
}
}
Using the client:
public class SomeApplicationClass()
{
SrvClient Client;
DispatcherTimer PingTimer;
public SomeClass()
{
BasicHttpBinding binding = new BasicHttpBinding();
EndpointAddress endpoint = new EndpointAddress(
"http://...:8000/Service/Address");
OutpostClient = new OutpostRemoteClient(binding, endpoint);
// pingTimer setup
}
// async voids are scary, make sure you handle exceptions inside this function
public async void PingTimer_Tick()
{
try
{
await Client.Ping();
// ping succeeded, do stuff
}
catch // specify exceptions here
{
// ping failed, do stuff
}
}
public async Task DoTheLongRunningOperation()
{
// set busy variables here etc.
string response = await Client.LongRunningOperation();
// handle the response status here
}
}
Also this answer seems relevant.

Async WCF self hosted service

My objective is to implement an asynchronous self hosted WCF service which will run all requests in a single thread and make full use of the new C# 5 async features.
My server will be a Console app, in which I will setup a SingleThreadSynchronizationContext, as specified here, create and open a ServiceHost and then run the SynchronizationContext, so all the WCF requests are handled in the same thread.
The problem is that, though the server was able to successfully handle all requests in the same thread, async operations are blocking the execution and being serialized, instead of being interlaced.
I prepared a simplified sample that reproduces the issue.
Here is my service contract (the same for server and client):
[ServiceContract]
public interface IMessageService
{
[OperationContract]
Task<bool> Post(String message);
}
The service implementation is the following (it is a bit simplified, but the final implementation may access databases or even call other services in asynchronous fashion):
public class MessageService : IMessageService
{
public async Task<bool> Post(string message)
{
Console.WriteLine(string.Format("[Thread {0} start] {1}", Thread.CurrentThread.ManagedThreadId, message));
await Task.Delay(5000);
Console.WriteLine(string.Format("[Thread {0} end] {1}", Thread.CurrentThread.ManagedThreadId, message));
return true;
}
}
The service is hosted in a Console application:
static void Main(string[] args)
{
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
using (ServiceHost serviceHost = new ServiceHost(typeof(MessageService)))
{
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
serviceHost.Open();
syncCtx.Run();
serviceHost.Close();
}
}
As you can see, the first thing I do is to setup a single threaded SynchronizationContext. Following, I create, configure and open a ServiceHost. According to this article, as I've set the SynchronizationContext prior to its creation, the ServiceHost will capture it and all the client requests will be posted in the SynchronizationContext. In the sequence, I start the SingleThreadSynchronizationContext in the same thread.
I created a test client that will call the server in a fire-and-forget fashion.
static void Main(string[] args)
{
EndpointAddress ep = new EndpointAddress(address);
NetNamedPipeBinding binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
IMessageService channel = ChannelFactory<IMessageService>.CreateChannel(binding, ep);
using (channel as IDisposable)
{
while (true)
{
string message = Console.ReadLine();
channel.Post(message);
}
}
}
When I execute the example, I get the following results:
Client
Server
The messages are sent by the client with a minimal interval ( < 1s).
I expected the server would receive the first call and run it in the SingleThreadSynchronizationContext (queueing a new WorkItem. When the await keyword was reached, the SynchronizationContext would be once again captured, the continuation posted to it, and the method would return a Task at this point, freeing the SynchronizationContext to deal with the second request (at least start dealing with it).
As you can see by the Thread's id in the server log, the requests are being correctly posted in the SynchronizationContext. However, looking at the timestamps, we can see that the first request is being completed before the second is started, what totally defeats the purpose of having a async server.
Why is that happening?
What is the correct way of implementing a WCF self hosted async server?
I think the problem is with the SingleThreadSynchronizationContext, but I can't see how to implement it in any other manner.
I researched the subject, but I could not find more useful information on asynchronous WCF service hosting, especially using the Task based pattern.
ADDITION
Here is my implementation of the SingleThreadedSinchronizationContext. It is basically the same as the one in the article:
public sealed class SingleThreadSynchronizationContext
: SynchronizationContext
{
private readonly BlockingCollection<WorkItem> queue = new BlockingCollection<WorkItem>();
public override void Post(SendOrPostCallback d, object state)
{
this.queue.Add(new WorkItem(d, state));
}
public void Complete() {
this.queue.CompleteAdding();
}
public void Run(CancellationToken cancellation = default(CancellationToken))
{
WorkItem workItem;
while (this.queue.TryTake(out workItem, Timeout.Infinite, cancellation))
workItem.Action(workItem.State);
}
}
public class WorkItem
{
public SendOrPostCallback Action { get; set; }
public object State { get; set; }
public WorkItem(SendOrPostCallback action, object state)
{
this.Action = action;
this.State = state;
}
}
You need to apply ConcurrencyMode.Multiple.
This is where the terminology gets a bit confusing, because in this case it doesn't actually mean "multi-threaded" as the MSDN docs state. It means concurrent. By default (single concurrency), WCF will delay other requests until the original operation has completed, so you need to specify multiple concurrency to permit overlapping (concurrent) requests. Your SynchronizationContext will still guarantee only a single thread will process all the requests, so it's not actually multi-threading. It's single-threaded concurrency.
On a side note, you might want to consider a different SynchronizationContext that has cleaner shutdown semantics. The SingleThreadSynchronizationContext you are currently using will "clamp shut" if you call Complete; any async methods that are in an await are just never resumed.
I have an AsyncContext type that has better support for clean shutdowns. If you install the Nito.AsyncEx NuGet package, you can use server code like this:
static SynchronizationContext syncCtx;
static ServiceHost serviceHost;
static void Main(string[] args)
{
AsyncContext.Run(() =>
{
syncCtx = SynchronizationContext.Current;
syncCtx.OperationStarted();
serviceHost = new ServiceHost(typeof(MessageService));
Console.CancelKeyPress += Console_CancelKeyPress;
var binding = new NetNamedPipeBinding(NetNamedPipeSecurityMode.None);
serviceHost.AddServiceEndpoint(typeof(IMessageService), binding, address);
serviceHost.Open();
});
}
static void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
if (serviceHost != null)
{
serviceHost.BeginClose(_ => syncCtx.OperationCompleted(), null);
serviceHost = null;
}
if (e.SpecialKey == ConsoleSpecialKey.ControlC)
e.Cancel = true;
}
This will translate Ctrl-C into a "soft" exit, meaning the application will continue running as long as there are client connections (or until the "close" times out). During the close, existing client connections can make new requests, but new client connections will be rejected.
Ctrl-Break is still a "hard" exit; there's nothing you can do to change that in a Console host.

Categories

Resources