Je développe une application ASP.NET Web Api avec C # et .Net Framework 4.7.
J'ai une méthode dans un contrôleur que je veux exécuter par un seul thread à la fois. En d'autres termes, si quelqu'un appelle cette méthode, un autre appel doit attendre que la méthode soit terminée.
J'ai trouvé cette SO réponse qui pourrait faire l'affaire. Mais ici, il utilise une file d'attente et je ne sais pas comment le faire pour consommer cette file d'attente. Dans cette réponse, explique que je peux créer un service Windows pour utiliser la file d'attente, mais que je ne souhaite pas ajouter une autre application à ma solution.
J'ai pensé ajouter un verrou à la méthode Web Api comme ceci:
[HttpPut]
[Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
{
lock
{
string errorMsg = "Cannot set commissioning.";
HttpResponseMessage response = null;
bool serverFound = true;
try
{
[ ... ]
}
catch (Exception ex)
{
_log.Error(ex.Message);
response = Request.CreateResponse(HttpStatusCode.InternalServerError);
response.ReasonPhrase = errorMsg;
}
return response;
}
}
Mais je ne pense pas que ce soit une bonne solution car cela pourrait bloquer beaucoup d'appels en attente s'il y a un problème lors de l'exécution de la méthode et que je vais perdre tous les appels en attente ou peut-être que je me trompe et que les appels (threads) vont attendre jusqu'à ce que les autres finissent. En d'autres termes, je pense que si je l'utilisais, je pourrais aboutir à une impasse.
J'essaie cela parce que je dois exécuter les appels dans le même ordre que je les reçois. Regardez ce journal d'actions:
2017-06-20 09:17:43,306 DEBUG [12] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38441110778119919475, withChildren : False
2017-06-20 09:17:43,494 DEBUG [13] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38561140779115949572, withChildren : False
2017-06-20 09:17:43,683 DEBUG [5] WebsiteAction - ENTERING PublicController::SendCommissioning , serial : 38551180775118959070, withChildren : False
2017-06-20 09:17:43,700 DEBUG [12] WebsiteAction - EXITING PublicController::SendCommissioning
2017-06-20 09:17:43,722 DEBUG [5] WebsiteAction - EXITING PublicController::SendCommissioning
2017-06-20 09:17:43,741 DEBUG [13] WebsiteAction - EXITING PublicController::SendCommissioning
Je reçois trois appels avant qu'aucun d'entre eux ne se termine: les threads [12], [13] and [5]
. Mais le dernier se termine avant le second : [12], [5] and [13]
.
J'ai besoin d'un mécanisme pour ne pas permettre cela.
Que puis-je faire pour que les appels soient traités dans le même ordre que je les ai passés?
Votre solution de verrouillage devrait fonctionner correctement. Si la demande échoue, le verrou sera libéré et les autres demandes en attente pourront alors entrer dans le verrou. Une impasse ne se produira pas.
Le seul problème de cette solution est que les demandes Web resteront suspendues pendant de longues périodes (ce qui peut entraîner des délais d'attente de la part du client).
public class MyApi : ApiController
{
public static readonly object LockObject = new object();
[HttpPut]
[Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
{
lock ( LockObject )
{
//Do stuff
}
}
}
Pour résoudre le problème des demandes en attente, vous devez utiliser une file d'attente et interroger le serveur principal (ou, si vous le souhaitez, essayer SignalR) jusqu'à la fin de votre travail. Par exemple:
//This is a sample with Request/Result classes (Simply implement as you see fit)
public static class MyBackgroundWorker
{
private static ConcurrentQueue<KeyValuePair<Guid, Request>> _queue = new ConcurrentQueue<KeyValuePair<Guid, Result>>()
public static ConcurrentDictionary<Guid, Result> Results = new ConcurrentDictionary<Guid, Result>();
static MyBackgroundWorker()
{
var thread = new Thread(ProcessQueue);
thread.Start();
}
private static void ProcessQueue()
{
KeyValuePair<Guid, Request> req;
while(_queue.TryDequeue(out req))
{
//Do processing here (Make sure to do it in a try/catch block)
Results.TryAdd(req.Key, result);
}
}
public static Guid AddItem(Request req)
{
var guid = new Guid();
_queue.Enqueue(new KeyValuePair(guid, req));
return guid;
}
}
public class MyApi : ApiController
{
[HttpPut]
[Route("api/Public/SendCommissioning/{serial}/{withChildren}")]
public HttpResponseMessage SendCommissioning(string serial, bool withChildren)
{
var guid = MyBackgroundWorker.AddItem(new Request(serial, withChildren));
return guid;
}
[HttpGet]
[Route("api/Public/GetCommissioning/{guid}")]
public HttpResponseMessage GetCommissioning(string guid)
{
if ( MyBackgroundWorker.Results.TryRemove(new Guid(guid), out Result res) )
{
return res;
}
else
{
//Return result not done
}
}
}
Je suppose que vous pouvez verrouiller sur différents niveaux, avec votre approche étant un.
J'ai rencontré un système (ou une application Web) qui utilise Redis en tant que service externe. Dans Redis, nous sauvegardons une clé pour la demande. Dans votre cas, je suppose que cela pourrait être le nom de la méthode. Avec cette approche, nous avons d’abord un filtre d’action qui vérifie s’il existe un verrou (dialogue avec redis) puis bloque la demande.
La bonne chose avec redis est qu’il est très rapide et vous permet de spécifier un délai d’attente au bout duquel la clé disparaîtra. Cela empêche d'être bloqué avec le verrou pour toujours.