Quantcast
Viewing latest article 7
Browse Latest Browse All 10

MongoDB Client-Trigger mit Reactive Extensions (Rx)

Ich bin also nicht so der Datenbanktyp, deshalb habe ich mich beim aktuellen Projekt für NoSQL entschieden. Ich denke man findet nur in einem wirklichen Projekt die Schmerzen einer Technologie. Ich will auch nicht in JavaScript programmieren, also ist meine Software in C# implementiert.

Ziel

Eine meiner ersten Anforderungen war, einen clientseitigen Trigger zu bekommen, der von der Datenbank ausgelöst wird, wenn ein Datensatz geändert wird – kann die Datenbank natürlich nicht so. Damit’s später auch schön zu verwenden ist wollte ich es mit den Reactive Extensions implementieren.

So soll’s verwendet werden:

      var elementsChanged = new MongoTrigger(dbClient)
.Where(trigger => trigger.Context == "MyDatabase.Elements");
      elementsChanged.Subscribe(trigger => MessageBox.Show(trigger.ToString()));

Ich habe eine Datenbank “MyDatabase” mit einer Collection “Elements”. Immer wenn Änderungen in dieser Collection erfolgen soll die angegebene Funktion (hier Lambda mit MessageBox) ausgeführt werden.

Der Vorteil ist die Verwendung der Reactive Extensions mit deren Hilfe ganz Einfach eine Filterung über Linq oder die Ausführung auf einem anderen Thread mit ObserveOn möglich.

Recherche

Nach kurzer suche im Internet findet man den Hinweis, dass alle Änderungen der Datenbank bei replizierten Installationen in der Collection oplog gespeichert werden.

Diese Implementierung erfordert, dass die Datenbank als Replica Set konfiguriert ist! (Mehr zu Replikation)

Der nächste Hinweis ist die Existenz von sogenannten tailablen Cursorn. Alles zusammen ergibt eine Abfrage, die bei neuen Einträgen das nächste Ergebnis liefert. Das ist im Prinzip schon die Anforderung für IObservable!

Da die Abfrage natürlich im MoveNext blockiert bis der nächste Eintrag erzeugt wird ist ein Backgroundworker notwendig der das asynchron erledigt.

Implementierung

Damit die Ereignisse alle Änderungsinformationen mitliefern können also zunächst mal eine TriggerArgs Klasse. Damit diese universell eingesetzt werden kann ist die id als string angegeben.

using MongoDB.Bson;
 
namespace ReactiveMongo
{
  
class MongoTriggerArgs
  {
    
public string Context { get; private set
; }
    
public string Operation { get; private set
; }
    
public string Id { get; private set
; }
    
public BsonDocument Document { get; private set
; }
     public MongoTriggerArgs(string context, string operation, string id, BsonDocument document)
     {
       Context = context;
       Operation = operation;
       Id = id;
       Document = document;
     }
    
public override string
ToString()
     {
      
return string.Format("{0}:{1}[{2}]", Operation, Context, Id);
     }
   } }

Als Context wird die Datenbank und die Collection mit Punkt getrennt angegeben (siehe oplog). Operation wird als “u”, “i”, “d” für update, insert, delete angegeben.

In Document steht der gesamte neue Datensatz zur Verfügung.

Hier also die Implementierung:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
 
namespace ReactiveMongo
{
  
class MongoTrigger : IObservable<MongoTriggerArgs>, IDisposable
  {
    
private readonly List<IObserver<MongoTriggerArgs
>> observers;
    
private readonly MongoCollection
opLog;
    
private readonly BackgroundWorker
trigger;
    
public MongoTrigger(MongoClient
client)
     {
       observers =
new List<IObserver<MongoTriggerArgs
>>();
      
var local = client.GetServer().GetDatabase("local"
);
       opLog = local.GetCollection(
"oplog.rs"
);
       trigger =
new BackgroundWorker { WorkerSupportsCancellation = true
};
       trigger.DoWork += TriggerOnDoWork;
       trigger.RunWorkerAsync();
     }
    
private void TriggerOnDoWork(object sender, DoWorkEventArgs
doWorkEventArgs)
     {
var tmax = opLog.FindAllAs<BsonDocument>()
.SetSortOrder(SortBy.Descending(new string[] {"ts"}))
.SetLimit(1)
.First();
var timeStamp = (BsonTimestamp)tmax["ts"];
      
while
(!trigger.CancellationPending)
       {
        
var cursor = opLog.FindAs<BsonDocument>(Query.GTE("ts"
, timeStamp))
                     .SetFlags(
QueryFlags.AwaitData | QueryFlags.TailableCursor | QueryFlags
.NoCursorTimeout)
                     .SetSortOrder(
SortBy.Ascending("$natural"
));
        
using (var enumerator = (MongoCursorEnumerator<BsonDocument
>)cursor.GetEnumerator())
         {
enumerator.MoveNext();
          
while
(!trigger.CancellationPending)
           {
            
if
(enumerator.MoveNext())
             {
              
var
document = enumerator.Current;
              
if (document != null
)
               {
                
Debug
.WriteLine(document.ToString());
                
var context = document["ns"
].AsString;
                
var operation = document["op"
].AsString;
                
var doc = document["o"
].AsBsonDocument;
                
var id = doc["_id"
].AsBsonValue.ToString();
                 NextTrigger(
new MongoTriggerArgs
(context, operation, id, doc));
                 timeStamp = (
BsonTimestamp)document["ts"
];
               }
             }
            
else
            {
              
if
(enumerator.IsDead)
               {
                
break
;
               }
              
if
(!enumerator.IsServerAwaitCapable)
               {
                
Thread.Sleep(TimeSpan
.FromMilliseconds(100));
               }
             }
           }
         }
       }
     }
    
public IDisposable Subscribe(IObserver<MongoTriggerArgs
> observer)
     {
      
if
(!observers.Contains(observer))
         observers.Add(observer);
      
return new Unsubscriber
(observers, observer);
      }
    
private class Unsubscriber : IDisposable
    {
      
private readonly List<IObserver<MongoTriggerArgs
>> unsubscribeObservers;
      
private readonly IObserver<MongoTriggerArgs
> unsubscribeObserver;
      
public Unsubscriber(List<IObserver<MongoTriggerArgs>> observers, IObserver<MongoTriggerArgs
> observer)
       {
         unsubscribeObservers = observers;
         unsubscribeObserver = observer;
       }
      
public void
Dispose()
       {
        
if (unsubscribeObserver != null
&& unsubscribeObservers.Contains(unsubscribeObserver))
           unsubscribeObservers.Remove(unsubscribeObserver);
       }
     }
    
private void NextTrigger(MongoTriggerArgs
args)
     {
      
foreach (var observer in
observers)
       {
         observer.OnNext(args);
       }
     }
    
public void
EndTransmission()
     {
      
foreach (var observer in
observers.ToArray())
        
if
(observers.Contains(observer))
           observer.OnCompleted();
       observers.Clear();
     }
    
public void
Dispose()
     {
      
if (trigger != null)
       {
         trigger.CancelAsync();
       }
     }
   } }

Dies ist die erste Version der Implementierung, in ein paar Stunden erstellt. Ich denke, dass sich beim konkreten Einsatz noch Änderungen und Erweiterungen ergeben.

Link: Daniel Weber: HACK: Creating triggers for MongoDB

Ich bin dankbar für Anregungen…


Viewing latest article 7
Browse Latest Browse All 10