Windows Azure fornisce un servizio chiamato Service Bus per implementare il messaging direttamente in cloud. Per avere maggiori informazioni riguardanti il messaging su Azure,
leggere qui.
In questi giorni sto pensando a come implementare un notification server e sembra che il servizio suddetto possa proprio essere di aiuto.
L'articolo descrive i concetti su "Queues, Topics e Subscriptions":
"These “brokered” messaging capabilities can be thought of as asynchronous, or decoupled messaging features that support publish-subscribe, temporal decoupling, and load balancing scenarios using the Service Bus messaging fabric. Decoupled communication has many advantages; for example, clients and servers can connect as needed and perform their operations in an asynchronous fashion."
Sembra proprio quello che mi serve. Ok, iniziamo a scrivere un progetto di test in modo da capire come si sviluppa una cosa del genere. Sono un dba, quindi apprezzate il mio sforzo e ovviamente, correggetemi ovunque ci fossero imprecisioni
Introduzione
Ci serve un progetto, possibilmente con Visual Studio 2012 (anche se c'è il template per 2010) con almeno tre progetti, il sender, il receiver ed un model con cui lavorare (che poi è il tipo del messaggio). Utilizzeremo il template proposto proprio da Visual Studio per la gestione di Queues (code) per poi convertirlo in uno che utilizza Topics/Subscriptions (Argomenti e sottoscrizioni). Approfondiremo il loro significato più tardi.
Step 1 - Prerequisiti
Di default, la versione corrente di Visual Studio 2012 non fornisce fin da subito. Quindi è necessario scaricare l'SDK direttamente con il web platform installer. Selezionando il linguaggio che si vuole utilizzare (nel mio caso C#), sotto la voce Cloud, è sufficiente fare doppio click sulla voce "SDK Download". La web platform scaricherà circa 40MB.
Step 2 - Nuovo progetto con Visual Studio
Aggiungendo il progetto cloud (che ho chiamato MessagingTest), la relativa soluzione verrà creata (MessagingTest anch'essa). Durante questo processo comparirà un popup in cui ci viene chiesto di scegliere che "Role" aggiungere. Noi avremo bisogno di un Worker Role, che possiamo definire come un'applicazione di Back-End (se guardate bene, troverete anche Web Role, ovvero una tipica applicazione web di frontend). Il tipo di Worker che andremo ad aggiungere è quello denominato "Worker Role With Service Bus Queue". Come si può evincere dal nome, il progetto creerà un gestore per una coda, mentre a noi serve quella a topic (argomenti) e subscriptions (sottoscrizioni). Lo cambieremo in seguito. Intanto ecco il popup di esempio:
Il template aggiungerà nel progetto il riferimento a Microsoft.ServiceBus.dll e i namespace seguenti:
using Microsoft.ServiceBus;
using Microsoft.ServiceBus.Messaging;
Sono stati aggiunti anche due tipi di file
.cscfg e
.csdef (il cosiddetto Windows Azure Service Configuration System) sul progetto cloud. Questi file determinano la connection string per connettersi al NAMESPACE, preventivamente creato per utilizzare il service bus. In poche parole andremo a creare un endpoint al quale invieremo messaggi alla nostra struttura (Topic per noi), utilizzando la connection string e la chiave generata direttamente dal
Windows Azure Portal.
Step 3 - Configurazioni
Prendiamo ora il file ServiceConfigurationLocal.cscfg nel progetto cloud e andiamo a cambiare la connection string. Supponendo di avere un namespace <mynamespace> e una <key> formeremo una connection string così fatta:
Endpoint=sb://<mynamespace>.servicebus.windows.net/;SharedSecretIssuer=owner;SharedSecretValue=<key>
Step 4 - Creazione del modello di messaggio
Andiamo ora a creare un progetto di tipo Class Library che chiameremo MessagingModel e che conterrà la struttura del messaggio con cui vogliamo lavorare. Possiamo parlare di "Tipo del messaggio". Aggiungiamo il progetto alla stessa soluzione:
Il modello che andremo a creare è il seguente:
Tre semplici proprietà che descrivono la struttura del nostro messaggio di notifica. Come possiamo vedere al progetto è stato aggiunto un file NotificationModel.cs in cui abbiamo definito appunto, la struttura del messaggio.
Il modello verrà referenziato dal Worker Role (perchè dovrà ricevere il messaggio e tipizzarlo per gestirlo) e dal Sender (luogo in cui il messaggio verrà composto per l'invio).
Step 5 - Preparare il message receiver (Worker Role)
Quando creiamo questo tipo di progetto, buona parte della struttura è praticamente completa. Ci sono già tre metodi,
OnStart(),
OnStop() e
Run(). Abbiamo già la definizione della coda, la sua creazione, il caricamento delle configurazioni, la parte in cui si riceve il messaggio e così via. Sembra semplice, sì vero, ma ovviamente questo è un progetto di demo. Quando dovremo mettere mano, in base alle specifiche del consumer dell'item della coda, il progetto potrebbe complicarsi notevolmente. Pensate infatti alle varie logiche da applicare per lo smistamento e la gestione del messaggio, oppure all'applicazione di un log o di procedure di auditing, di cache e così via. In aggiunta, è possibile cambiare anche le specifiche di lettura (abbiamo
PeekLock o "lettura non distruttiva" e la
ReceiveAndDelete o "lettura distruttiva").
Tuttavia il progetto creato, come dicevamo, gestisce una coda e non un Topic/Subscriber. Lo scopo di questo post è convertire il tipo fornito nativamente in uno a Topic. Per capire la differenza, ecco due immagini:
Queues (code)
I vari sender inviano un messaggio alla coda, e il consumer legge un messaggio. Ogni messaggio è processato da un consumer solo alla volta.
Topics (argomenti)
I sender inviano i messaggi al topic e ogni consumer crea una sottoscrizione, creando, di fatto, una relazione uno a molti.
Ora passiamo al refactor del Worker role, partendo dalla testata, come impostarla?
Avremo bisogno di un nome topic, di un nome sottoscrizione e il client per gestire la messaggistica:
public class WorkerRole : RoleEntryPoint
{
const string TopicName = "MyFirstTopic"; // Topic name
const string SubscriptionName = "MySubscription"; // Subscription name
// client for managing messages
SubscriptionClient Client;
// stop flag
bool IsStopped;
[...]
SubscriptionName e TopicName verranno utilizzati anche nel progetto che invia i messaggi (più avanti nel post).
Ora, come definiamo il metodo OnStart()?
In questo metodo creeremo la sottoscrizione e utilizzeremo la connection string indicata nei file di configurazione:
public override bool OnStart()
{
// Set the maximum number of concurrent connections
ServicePointManager.DefaultConnectionLimit = 12;
// Get configs from config file
string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
// Inserts a new topic if not exists
var namespaceManager = NamespaceManager.CreateFromConnectionString(connectionString);
if (!namespaceManager.TopicExists(TopicName))
{
namespaceManager.CreateTopic(TopicName);
}
// Creates the subscription if not exists
if (!namespaceManager.SubscriptionExists(TopicName, SubscriptionName))
{
namespaceManager.CreateSubscription(TopicName, SubscriptionName);
}
// Creates the subscription client
Client = SubscriptionClient.CreateFromConnectionString(connectionString, TopicName, SubscriptionName);
IsStopped = false;
return base.OnStart();
}
Il metodo
CreateSubscription, utilizzato con due parametri, crea una sottoscrizione senza filtro. Per applicare filtri, è possibile utilizzare la classe
SQLFilter. Ad esempio:
// The MessageType property must be set on the message when sending
namespaceManager.CreateSubscription(TopicName, SubscriptionName, new SqlFilter("MessageType = 'NOTIFICATION'"));
Il filtro deve essere impostato prima di inviare il messaggio, e chi lo gestisce deve poi applicarlo.
Come cambiare il metodo OnStop()?
Non c'è da cambiare nulla visto che disalloca già correttamente quello che utilizziamo.
Come leggere il messaggio ricevuto nel metodo Run()?
Utilizzeremo come detto la PeekLock (che è il metodo di lettura di default). In questo modo i messaggi letti non verranno "distrutti" e rimarranno nel topic. Il metodo tuttavia può essere cambiato utilizzando gli overload del metodoCreateFromConnectionString() utilizzato nell'OnStart(). Applicheremo qui di seguito un ciclo infinito che "attende" la ricezione del messaggio:
while (!IsStopped)
{
try
{
// Receive the message
BrokeredMessage receivedMessage = Client.Receive();
if (receivedMessage != null)
{
// Process the message
// Do something..
receivedMessage.Complete();
}
}
[...]
Il resto del metodo è costituito dalla gestione delle eccezioni (anche, ad esempio, per le retry). Quello che manca è il "Do something.." e proprio qui andremo ad aggiungere la Trace del nostro messaggio:
if (receivedMessage != null)
{
// Process the message (cast into our model)
NotificationModel model = receivedMessage.GetBody<NotificationModel>();
// Here's the message
Trace.WriteLine("Message id", model.NotificationId.ToString());
Trace.WriteLine("Message description", model.NotificationDescription);
Trace.WriteLine("Message date", model.NotificationDate.ToShortDateString());
receivedMessage.Complete();
}
La prima cosa che si nota è la get del messaggio. GetBody<T>() consente di castare l'oggetto ricevuto nel tipo che abbiamo disegnato appositamente. Molto comodo. T = NotificationModel.
Step 6 - Inviare il messaggio di tipo NotificationModel
Siamo quindi pronti per inviare il messaggio. Possiamo aggiungere un qualunque tipo di progetto (client), ma la cosa più semplice è creare una console application. In quest'ultima andremo a referenziare il nostro model e le due dllMicrosoft.WindowsAzure.Configuration.dll e Microsoft.ServiceBus.dll. A questo punto, nell'app.config della console andremo ad aggiungere la connectionstring nella sezione apposita, come segue:
<appSettings>
<add key="Microsoft.ServiceBus.ConnectionString" value="Endpoint=sb://<mynamespace>.servicebus.windows.net/;SharedSecretIssuer=owner;SharedSecretValue=<key>" />
</appSettings>
ed ecco il metodo Main():
NotificationModel model = new NotificationModel();
// prepare the message
model.NotificationDate = DateTime.Now;
model.NotificationDescription = "This is my first message";
model.NotificationId = 1;
// get the connection string from config (app.config in this sample)
string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
TopicClient Client = TopicClient.CreateFromConnectionString(connectionString, TopicName);
// Create message, passing our model
BrokeredMessage message = new BrokeredMessage(model);
// Send message to the topic
Client.Send(message);
Come dicevamo prima, per creare il valore su cui filtrare poi in sottoscrizione, basta aggiungere una proprietà al volo, appena prima di inviare:
// Message type for filtering on subscription (optional)
message.Properties["MessageType"] = "NOTIFICATION";
Facciamo partire il worker role. Verrà creato il topic e la sottoscrizione relativa. Il debugger rimarrà in attesa della ricezione dei messaggi. Parte quindi il ciclo infinito.
Possiamo monitorare il worker utilizzando una UI chiamata "Windows Azure Compute and Storage emulator UI". Noterete nel systray un'icona dalla quale è possibile aprire la user interface (tasto destro -> "Show Compute Emulator UI"):
Come possiamo vedere il worker rimane in attesa. Per inviare il messaggio, andiamo sulla bin del progetto console ed eseguiamo il SendTest.exe. In base a come abbiamo configurato le nostre strutture, invieremo al "MyFirstTopic", con una "MyFirstSubscription", un messaggio di tipo NotificationModel. Il messaggio, a sua volta, avrà potenzialmente (se specificato) una proprietà "MessageType" come valore di filtro. Una volta che il messaggio è arrivato, vedremo quanto segue sul monitor:
Funziona! Vedo la traccia! Questo è il monitor locale, ma sul Windows Azure Portal abbiamo il monitor del cloud "reale":
se siete interessati a questo approccio nelle vostre applicazioni vi consiglio i seguenti link: