Bibliothèque open source pour un traitement d'arrière-plan simple et distribué pour .NET, alimenté par RabbitMQ Message Broker. La documentation, une fois terminée, sera disponible sur le site Web MassiveJobs.NET.
Si vous n'avez pas d'installation existante de RabbitMQ, le moyen le plus simple est de le démarrer dans un conteneur. La commande suivante commencera RabbitMQ dans un conteneur qui sera immédiatement supprimé lors de l'arrêt .
docker run -- rm -- hostname rabbit - test -- name rabbit - test - d - p 15672 : 15672 - p 5672 : 5672 rabbitmq:managementMaintenant, vous devriez pouvoir accéder à l'interface utilisateur de gestion RabbitMQ dans votre navigateur sur: http: // localhost: 15672 Adresse. Vous pouvez vous connecter avec l'invité d' invité et le mot de passe du nom d'utilisateur, si vous souhaitez surveiller les connexions, les files d'attente, etc.
Nous utiliserons .NET Core 3.1 CLI pour ce démarrage rapide, mais vous pouvez également le faire dans Visual Studio, avec .NET Core ou avec .NET Framework 4.6.1 ou version ultérieure.
Créez un dossier pour le projet.
mkdir MassiveJobs.QuickStart
cd MassiveJobs.QuickStartCréez un nouveau projet d'application de console.
dotnet new consoleTestez le projet échafaudé.
dotnet run Vous devriez voir Hello World! Après quelques secondes.
Ajoutez une référence de package à MassiveJobs.RabbitMqBroker .
dotnet add package MassiveJobs.RabbitMqBrokerUtilisez votre éditeur préféré pour ouvrir Program.cs et entrez ce code. Les commentaires dans le code devraient être suffisants pour vous donner une idée de base de ce qui se passe.
using System ;
using MassiveJobs . Core ;
using MassiveJobs . RabbitMqBroker ;
namespace MassiveJobs . QuickStart
{
/// <summary>
/// This is a "job" class.
/// It will be instantiated every time a message is received, and Perform will be called.
/// It inherits from Job<TJob, TArgs> generic class, where TJob specifies the type of the job,
/// and TArgs specifies the type of the parameter expected by the Perform method.
///
/// In the example below, TArgs is a string, but it can be a custom class with multiple properties.
/// TArgs instances will be serialized (System.Text.Json by default) as a part of the job,
/// before it gets sent to the RabbitMQ.
/// </summary>
public class MessageReceiver : Job < MessageReceiver , string >
{
public override void Perform ( string message )
{
Console . WriteLine ( "Job performed: " + message ) ;
}
}
class Program
{
private static void Main ( )
{
Console . WriteLine ( "1: Worker" ) ;
Console . WriteLine ( "2: Publisher" ) ;
Console . Write ( "Choose 1 or 2 -> " ) ;
var startWorkers = Console . ReadLine ( ) != "2" ;
// We are not starting job workers if '2' is selected.
// This is not mandatory, an application can run job workers
// and publish jobs using the same MassiveJobs instance.
JobsBuilder . Configure ( )
. WithRabbitMqBroker ( )
. Build ( startWorkers ) ;
if ( startWorkers )
{
RunWorker ( ) ;
}
else
{
RunPublisher ( ) ;
}
JobsBuilder . DisposeJobs ( ) ;
}
private static void RunWorker ( )
{
Console . WriteLine ( "Initialized job worker." ) ;
Console . WriteLine ( "Press Enter to end the application." ) ;
Console . ReadLine ( ) ;
}
private static void RunPublisher ( )
{
Console . WriteLine ( "Initialized job publisher" ) ;
Console . WriteLine ( "Write the job name and press Enter to publish it (empty job name to end)." ) ;
while ( true )
{
Console . Write ( "> " ) ;
var message = Console . ReadLine ( ) ;
if ( string . IsNullOrWhiteSpace ( message ) ) break ;
// notice that Publish is a static method on our MessageReceiver class
// it is available because MessageReceiver inherits from Job<TJob, TArgs>
MessageReceiver . Publish ( message ) ;
}
}
}
}Démarrez trois invites de commande différentes (ou coquilles d'alimentation). Deux seront utilisés comme travailleurs, et un sera utilisé comme éditeur.
Pour démarrer l'application, allez dans le dossier du projet et exécutez:
dotnet run Type 1 et appuyez sur Entrée pour démarrer un travailleur, type 2 et appuyez sur ENTER pour démarrer l'éditeur. Lorsque vous entrez des messages dans la console de l'éditeur, vous les remarquerez être traités dans l'un ou l'autre travailleur, mais pas les deux. En effet, les emplois sont distribués entre les travailleurs.
Notez que vous pouvez également démarrer plusieurs éditeurs.
Les travailleurs et les éditeurs peuvent être sur différentes machines, tant qu'ils peuvent accéder au serveur RabbitMQ.
Pour distribuer les travailleurs sur plusieurs machines, vous devrez configurer les informations sur le serveur RabbitMQ. Au minimum, cela signifie nom d'utilisateur, mot de passe, nom d'hôte (ou adresse IP) et le numéro de port (si votre serveur RabbitMQ est configuré pour écouter les connexions sur un port non standard). Dans l'exemple ci-dessus, nous n'avons pas configuré rien car les valeurs par défaut étaient suffisantes - nom d'utilisateur: guest , mot de passe: guest , nom d'hôte: localhost , port: -1 (= utilisez le port par défaut).
Par exemple, si votre serveur RabbitMQ s'exécute sur une machine avec le nom d'hôte rabbit.example.local , écoutant sur le numéro de port standard, et vous avez créé un utilisateur massive dans le lapin avec le mot de passe: d0ntUseTh!sPass , vous initialiseriez RabbitMqJobs comme celui-ci.
JobsBuilder . Configure ( )
. WithRabbitMqBroker ( s =>
{
s . HostNames = new [ ] { "rabbit.example.com" } ;
s . Username = "massive" ;
s . Password = "d0ntUseTh!sPass" ;
} )
. Build ( ) ;Ou, si vous ne voulez pas démarrer les fils de travail (c'est-à-dire pour utiliser le processus uniquement pour publier des travaux):
JobsBuilder . Configure ( )
. WithRabbitMqBroker ( s =>
{
//...
} )
. Build ( false ) ;Vous pouvez maintenant déployer des travailleurs (et des éditeurs) sur plusieurs machines et les exécuter. Si la connectivité réseau fonctionne (les pare-feu ouverts, etc.), tout devrait fonctionner. Les emplois seraient acheminés vers des travailleurs de manière à la ronde. Gardez à l'esprit que, par défaut, chaque application MassiveJobs commence deux threads de travail. Cela signifie que si vous avez 3 machines, chacune exécutant une application massivejobs, alors la distribution des travaux ressemblerait à ceci:
Vous avez peut-être remarqué, dans l'exemple de démarrage rapide, lorsque nous avions exécuté deux applications massivesjobs dans deux fenêtres Posershell, deux des messages iraient à une fenêtre, les deux suivants à l'autre fenêtre et ainsi de suite. Maintenant, vous connaissez la raison.
Ignorez cette section si votre application est en cours d'exécution dans un environnement hébergé .NET Core (ASP.NET Core Web Application ou Worker Service).
Il est très important de configurer la connexion dans votre application exécutant MassiveJobs, car c'est le seul moyen de voir des erreurs d'exécution massivejobs dans votre application. Il est aussi simple que d'installer un package approprié et de définir le JobLoggerFactory lors de l'initialisation, si vous utilisez l'une des bibliothèques d'enregistrement suivantes:
MassiveJobs.Logging.Log4Net )MassiveJobs.Logging.NLog )MassiveJobs.Logging.Serilog ) Par exemple, si vous souhaitez ajouter la journalisation log4net à l'exemple de démarrage rapide, installez d'abord le package MassiveJobs.Logging.Log4Net dans votre projet. Après cela, initialisez la bibliothèque log4net et enfin MassiveJobs.
//...
using MassiveJobs . Logging . Log4Net ;
//...
private static void Main ( )
{
InitializeLogging ( ) ;
Console . WriteLine ( "1: Worker" ) ;
Console . WriteLine ( "2: Publisher" ) ;
Console . Write ( "Choose 1 or 2 -> " ) ;
var startWorkers = Console . ReadLine ( ) != "2" ;
// We are not starting job workers if '2' is selected.
// This is not mandatory, an application can run job workers
// and publish jobs using the same MassiveJobs instance.
JobsBuilder . Configure ( )
. WithLog4Net ( )
. WithRabbitMqBroker ( )
. Build ( startWorkers ) ;
if ( startWorkers )
{
RunWorker ( ) ;
}
else
{
RunPublisher ( ) ;
}
}Vous devez implémenter vous-même «initialiselogging», car vous effectuez normalement une initialisation pour votre bibliothèque d'enregistrement. Par exemple, pour Log4Net, cela ne configurerait que l'appender de la console.
private static void InitializeLogging ( )
{
var patternLayout = new PatternLayout ( ) ;
patternLayout . ConversionPattern = "%date [%thread] %-5level %logger - %message%newline" ;
patternLayout . ActivateOptions ( ) ;
var hierarchy = ( Hierarchy ) LogManager . GetRepository ( Assembly . GetExecutingAssembly ( ) ) ;
hierarchy . Root . AddAppender ( new ConsoleAppender { Layout = patternLayout } ) ;
hierarchy . Root . Level = Level . Warn ;
hierarchy . Configured = true ;
}Maintenant, lorsque vous démarrez l'application de travailleur, vous devriez voir des messages d'enregistrement dans la console:
PS > dotnet run
1 : Worker
2 : Publisher
Choose 1 or 2 - > 1
2020 - 11 - 10 10 : 25 : 22 , 251 [ 1 ] WARN MassiveJobs.RabbitMqBroker.RabbitMqMessageConsumer - Connected
Initialized job worker.
Press Enter to end the application.Vous remarquerez que si vous démarrez l'application Publisher, il n'essaie pas de se connecter à RabbitMQ avant d'essayer d'envoyer les premiers messages. C'est parce que chaque application massivejobs conserve deux connexions avec le RabbitMQ, l'une pour la publication et l'autre pour la consommation de messages. Dans l'éditeur, nous ne commençons pas les travailleurs, donc consommer la connexion n'est pas initialisé.
PS > dotnet run
1 : Worker
2 : Publisher
Choose 1 or 2 - > 2
Initialized job publisher
Write the job name and press Enter to publish it (empty job name to end ).
> Hello
2020 - 11 - 10 10 : 27 : 22 , 954 [ 4 ] WARN MassiveJobs.RabbitMqBroker.RabbitMqMessagePublisher - Connected Pour utiliser MassiveJobs.RabbitMqBroker dans un environnement hébergé .net Core (ASP.NET Core, Worker Services) Installez le package suivant dans votre application:
dotnet add package MassiveJobs.RabbitMqBroker.Hosting Ensuite, dans votre classe de démarrage, lors de la configuration des services, appelez services.AddMassiveJobs() .
//...
using MassiveJobs . RabbitMqBroker . Hosting ;
namespace MassiveJobs . Examples . Api
{
public class Startup
{
public Startup ( IConfiguration configuration )
{
Configuration = configuration ;
}
public IConfiguration Configuration { get ; }
public void ConfigureServices ( IServiceCollection services )
{
//...
services . AddMassiveJobs ( )
. UseRabbitMqBroker ( ) ;
}
//...
}
} Cela enregistrera les services MassiveJobs requis et démarrera un service hébergé pour la gestion des travailleurs du travail. Vous pouvez maintenant publier des emplois à partir d'un contrôleur. Par exemple, si vous avez une entité Customer et que vous souhaitez envoyer un e-mail de bienvenue à un client nouvellement créé, vous pourriez avoir quelque chose comme ceci:
// POST: api/Customers
[ HttpPost ]
public ActionResult < Customer > PostCustomer ( Customer customer )
{
using var trans = _context . Database . BeginTransaction ( ) ;
_context . Customers . Add ( customer ) ;
_context . SaveChanges ( ) ;
if ( ! string . IsNullOrWhiteSpace ( customer . Email ) )
{
// send a welcome email after 5 seconds
SendWelcomeEmailJob . Publish ( customer . Id , TimeSpan . FromSeconds ( 5 ) ) ;
}
// do this last. If Job publishing to RabbitMq fails, we will rollback
trans . Commit ( ) ;
return CreatedAtAction ( "GetCustomer" , new { id = customer . Id } , customer ) ;
} Il est très important de garder à l'esprit que SendWelcomeEmailJob.Publish ne participe pas à la transaction _. Rabbitmqbroker pour MassiveJobs ne prend pas en charge les transactions. Mais, la méthode Publish lancera l'exception si la publication échoue (uniquement l'édition - n'envoyant pas réellement le courrier, ce qui est fait de manière asyncronique). Si la publication échoue, l'exception sera lancée et trans.Commit() ne sera jamais appelée et la transaction sera recouverte de Disser.
Esénétiellement, la publication d'un emploi est ici utilisée comme dernière ressource de validation .
Le SendWelcomeEmailJob pourrait ressembler à ceci:
public class SendWelcomeEmailJob : Job < SendWelcomeEmailJob , int >
{
private readonly ExamplesDbContext _context ;
public SendWelcomeEmailJob ( ExamplesDbContext context )
{
_context = context ;
}
public override void Perform ( int customerId )
{
using var trans = _context . Database . BeginTransaction ( ) ;
var customer = _context . Customers . Find ( customerId ) ;
if ( customer . IsEmailSent ) return ; // make the job idempotent
customer . IsEmailSent = true ;
// Do this before sending email, to lessen the chance of an exception on commit.
// Also, if optimistic concurrency is enabled, we will fail here, before sending the email.
// This way we avoid sending the email to the customer twice.
_context . SaveChanges ( ) ;
SendEmail ( customer ) ;
// Do this last. In case the SendEmail method fails, the transaction will be rolled back.
trans . Commit ( ) ;
}
private static void SendEmail ( Customer customer )
{
var mailMessage = new MailMessage
{
From = new MailAddress ( "[email protected]" ) ,
Body = $ "Welcome customer { customer . FirstName } { customer . LastName } " ,
Subject = "Welcome to examples.com"
} ;
mailMessage . To . Add ( customer . Email ) ;
using ( var client = new SmtpClient ( "smtp.examples.com" ) )
{
client . UseDefaultCredentials = false ;
client . Credentials = new NetworkCredential ( "username" , "password" ) ;
client . Send ( mailMessage ) ;
}
}
}Il y a plusieurs choses à noter ici:
customer.IsEmailSent est vérifié avant de faire quoi que ce soit. S'il est défini sur vrai, nous ne faisons rien (aucune exception n'est lancée, car l'exception ferait la bibliothèque massivejobs de planifier le travail pour les tentatives)SaveChanges() sur le contexte de la base de données avant d' envoyer l'e-mail afin qu'il puisse lancer des exceptions de concurrence qui reprogrammeront le travail pour plus tard (mais vous devez configurer des propriétés de concurrence sur vos intestins pour qu'il fonctionne). Cependant, dans ce cas particulier, notre classe d'emploi n'est pas entièrement idempotente. Il peut encore arriver que l'e-mail soit envoyé deux fois car le serveur de messagerie ne participe pas à la transaction. Si client.Send lance l'exception de temps mort , il n'est pas certain que l'e-mail a été réellement envoyé ou non. Le serveur de messagerie a peut-être reçu la demande, a mis en file d'attente le message de livraison, mais nous n'avons jamais obtenu la réponse en raison d'un problème de réseau temporaire. En d'autres termes, au moins une fois la livraison est garantie dans ce cas, pas exactement une fois .
Si seuls les changements de base de données étaient impliqués dans le travail, nous pourrions avoir exactement une fois des garanties. Mais même alors, la méthode Perform du travail peut être appelée deux fois, vous devez donc vous assurer que le travail est idempotent dans la méthode Perform (similaire à ce que nous avons fait avec IsEmailSent ).