web-dev-qa-db-ja.com

聞き始めた後の変更だけでなく、最初のドキュメントでMongoDBChangeStreamを再開するにはどうすればよいですか

このアプリの私の目標は、データベースを監視し、ドキュメントがデータベースに追加されたときにアクションをトリガーするロジックを作成することです(メールの送信など)。ただし、データベースに最初にデータが入力されたときにこのアプリケーションが起動しない可能性があるため、コレクションに追加された最初のドキュメントを指すResumeTokenを手動で作成して、最初の実行時に最初から開始できるようにするにはどうすればよいですか。最後に到達するまで変更を繰り返します。将来の再起動のためにlastChangeStreamDocumentからResumeTokenを保存する必要があることを認識していますが、「最初の実行」シナリオに興味があります。 enumerator.Reset();は正しいオプションでしたが、サポートされていないことを示す例外がスローされました。

https://github.com/mongodb/mongo-csharp-driver/blob/master/tests/MongoDB.Driver.Examples/ChangeStreamExamples.cs で提供されているテストに従い、正常に構成しました次のコードを使用した変更ストリーム

mongoClient = mongoClient ?? new MongoClient(ConnectionString);  //Create client object if it is null
IMongoDatabase sandboxDB = mongoClient.GetDatabase("SandboxDB");

var collection = sandboxDB.GetCollection<BsonDocument>("CollectionToMonitor");

try
{
    var cursor = collection.Watch();
    var enumerator = cursor.ToEnumerable().GetEnumerator();

    enumerator.MoveNext();  //Blocks until a record is UPDATED in the database
    var lastChangeStreamDocument = enumerator.Current;
    enumerator.Dispose();
    //lastChangeStreamDocument.FullDocument.Should().Be(document);

}
catch( Exception ex)
{
    Logger.WriteException(ex);
}

ただし、このコードでは、ドキュメントが更新されるまでenumerator.MoveNext()行がブロックされるため、変更ストリームを設定した後にのみ更新されたドキュメントへの参照を取得できます。

Local.oplogデータベースを検索して、コレクションに挿入された最初のドキュメントのUUIDを取得することを考えましたが、成功しましたが、この参照を、フィードできるResumeTokenオブジェクトに変換する方法がわかりません。ウォッチメソッド。


更新:

ResumeTokenは、タイムスタンプ、o._id ObjectID、およびoplogエントリのuiUUIDを含むBase64として保存されているようです。コードをもう少しトラバースする必要がありますが、このソースコードから表示されます( https://github.com/mongodb/mongo/blob/c906f6357d22f66d58e3334868025069c62bd97b/src/mongo/db/pipeline/resume_token_test.cpp )ResumeTokenにはさまざまな形式があること。この情報を使用して、データベースが期待する形式に一致する独自のResumeTokenを構築できることを願っています。


更新#2:

さらに調査した後、mongoのkey_stringを解析するためのコードに出くわしました github.com/mongodb/mongo/src/mongo/db/storage/key_string.cpp 。このファイルには、CTypeの定義が含まれています。 Base64をバイト配列にデコードした後、CType列挙型定義を使用して、独自のResumeTokenを構築する方法についてもう少し理解することができました。

次の例を考えてみましょう。ドキュメントを更新した後、ChangeStreamでResumeTokenをキャプチャしました。

glp9zsgAAAABRmRfaWQAZFp9zH40PyabFRwB/ABaEAQESw1YexhL967nKLXsT5Z+BA==

これはバイト配列にデコードされます:

82 5a 7d ce c8 00 00 00 01 46 64 5f 69 64 00 64 5a 7d cc 7e 34 3f 26 9b 15 1c 01 fc 00 5a 10 04 04 4b 0d 58 7b 18 4b f7 ae e7 28 b5 ec 4f 96 7e 04

私がデコードしたもの:

//Timestamp (of oplog entry??)
82    //CType::TimeStamp
5a 7d ce c8 00 00 00 01   //It appears to be expecting a 64b number
//I'm not sure why the last byte 0x01 unless it has something to do with little/bit endian
//Matching oplog doc has { ts: TimeStamp(1518194376, 1) }
//  that integer converts to 0x5A7DCEC8

//Unknown Object
46    //CType::Object
64 5f 69 64     //Either expecting a 32b value or null terminated
00    //Null terminator or divider

//Document ID
64    //CType::OID
5a 7d cc 7e 34 3f 26 9b 15 1c 01 fc  //o._id value from oplog entry
00    //OID expecting null terminated

//UUID
5a    //CType::BinData
10    //Length (16b)
04    //BinDataType of newUUID (from bsontypes.h)
04 4b 0d 58 7b 18 4b f7 ae e7 28 b5 ec 4f 96 7e  //UUID value from oplog entry
04    //Unknown byte. Perhaps end of ResumeToken, or end of UUID mark?

私が今抱えている問題は、コレクションに多くのoplogエントリがあり、oplogの最初のエントリのts、ui、o._id値を使用して、独自のResumeTokenを構築することです(不明な0x4664 5f69 6400ブロックをハードコーディングします。終了する0x04バイトの場合、サーバーはcollection.Watchを設定するときに、これを有効なResumeTokenとして受け入れます。ただし、enumerator.moveNext()呼び出しによって返されるドキュメントは、2番目ではなく常に3番目のoplogエントリを返します。

その12Byteブロックの目的を知らず、また2番目のエントリではなく3番目のエントリを指している理由も知らずに、本番環境でこれに依存することに神経質になっています。


更新#3:

問題のバイトのブロック:

46 64 5f 69 64 00

0x46 = CType::Object
0x64 = d
0x5F = _
0x69 = i
0x64 = d
0x00 = NULL

次のバイトブロックは、影響を受けるドキュメントのObjectId、つまり「_id」キーを示しています。では、「d」文字の意味は何ですか?

9
Jerren Saunders

私はこれに取り組んでいる間、追加情報で質問を更新してきました、そして私はそれが機能するように今それを完全につなぎ合わせることができました。

以下は、私が作成したコードです。

  1. Local.oplogコレクションで名前空間の最初のエントリを検索します
  2. そのoplogドキュメントからResumeTokenを生成します(2番目のエントリから再開します)
  3. それらの機能をテストするための例。

うまくいけば、このコードは同じことをしようとしている他の人にとって有益になるでしょう。

/// <summary>
/// Locates the first document for the given namespace in the local.oplog collection
/// </summary>
/// <param name="docNamespace">Namespace to search for</param>
/// <returns>First Document found in the local.oplog collection for the specified namespace</returns>
internal static BsonDocument GetFirstDocumentFromOpLog(string docNamespace)
{
    mongoClient = mongoClient ?? new MongoClient(ConnectionString);  //Create client object if it is null
    IMongoDatabase localDB = mongoClient.GetDatabase("local");
    var collection = localDB.GetCollection<BsonDocument>("oplog.rs");

    //Find the documents from the specified namespace (DatabaseName.CollectionName), that have an operation type of "insert" (The first entry to a collection must always be an insert)
    var filter = MongoDB.Bson.Serialization.BsonSerializer.Deserialize<BsonDocument>("{ $and: [ { 'ns': '" + docNamespace + "'}, { 'op': 'i'}] }");

    BsonDocument retDoc = null;
    try //to get the first document from the oplog entries
    {       
        retDoc = collection.Find<BsonDocument>(filter).First();
    }
    catch(Exception ex) { /*Logger.WriteException(ex);*/ }
    return retDoc;
}

/// <summary>
/// Takes a document from the OpLog and generates a ResumeToken
/// </summary>
/// <param name="firstDoc">BsonDocument from the local.oplog collection to base the ResumeToken on</param>
/// <returns>A ResumeToken that can be provided to a collection watch (ChangeStream) that points to the firstDoc provided</returns>
private static BsonDocument GetResumeTokenFromOpLogDoc(BsonDocument firstDoc)
{
    List<byte> hexVal = new List<byte>(34);

    //Insert Timestamp of document
    hexVal.Add(0x82);   //TimeStamp Tag
    byte[] docTimeStampByteArr = BitConverter.GetBytes(firstDoc["ts"].AsBsonTimestamp.Timestamp); //Timestamp is an integer, so we need to reverse it
    if (BitConverter.IsLittleEndian) { Array.Reverse(docTimeStampByteArr); }
    hexVal.AddRange(docTimeStampByteArr);

    //Expecting UInt64, so make sure we added 8 bytes (likely only added 4)
    hexVal.AddRange(new byte[] { 0x00, 0x00, 0x00, 0x01 }); //Not sure why the last bytes is a 0x01, but it was present in observed ResumeTokens

    //Unknown Object observed in a ResumeToken
    //0x46 = CType::Object, followed by the string "d_id" NULL
    //This may be something that identifies that the following value is for the "_id" field of the ObjectID given next
    hexVal.AddRange(new byte[] { 0x46, 0x64, 0x5F, 0x69, 0x64, 0x00 }); //Unknown Object, expected to be 32 bits, with a 0x00 terminator

    //Insert OID (from 0._id.ObjectID)
    hexVal.Add(0x64);   //OID Tag
    byte[] docByteArr = firstDoc["o"]["_id"].AsObjectId.ToByteArray();
    hexVal.AddRange(docByteArr);
    hexVal.Add(0x00);   //End of OID

    //Insert UUID (from ui) as BinData
    hexVal.AddRange(new byte[] { 0x5a, 0x10, 0x04 });   //0x5A = BinData, 0x10 is Length (16 bytes), 0x04 is BinDataType (newUUID)
    hexVal.AddRange(firstDoc["ui"].AsByteArray);

    hexVal.Add(0x04);   //Unknown marker (maybe end of resumeToken since 0x04 == ASCII 'EOT')

    //Package the binary data into a BsonDocument with the key "_data" and the value as a Base64 encoded string
    BsonDocument retDoc = new BsonDocument("_data", new BsonBinaryData(hexVal.ToArray()));
    return retDoc;
}


/// <summary>
/// Example Code for setting up and resuming to the second doc
/// </summary>
internal static void MonitorChangeStream()
{
    mongoClient = mongoClient ?? new MongoClient(ConnectionString);  //Create client object if it is null
    IMongoDatabase sandboxDB = mongoClient.GetDatabase("SandboxDB");
    var collection = sandboxDB.GetCollection<BsonDocument>("CollectionToMonitor");

    var options = new ChangeStreamOptions();
    options.FullDocument = ChangeStreamFullDocumentOption.UpdateLookup;

    try
    {
        var pipeline = new EmptyPipelineDefinition<ChangeStreamDocument<BsonDocument>>().Match("{ operationType: { $in: [ 'replace', 'insert', 'update' ] } }");  //Works

        //Build ResumeToken from the first document in the oplog collection
        BsonDocument resumeTokenRefDoc = GetFirstDocumentFromOpLog(collection.CollectionNamespace.ToString());
        if (resumeTokenRefDoc != null)
        {
            BsonDocument docResumeToken = GetResumeTokenFromOpLogDoc(resumeTokenRefDoc);
            options.ResumeAfter = docResumeToken;
        }

        //Setup the ChangeStream/Watch Cursor
        var cursor = collection.Watch(pipeline, options);
        var enumerator = cursor.ToEnumerable().GetEnumerator();

        enumerator.MoveNext();  //Blocks until a record is UPDATEd, REPLACEd or INSERTed in the database (thanks to the pipeline arg), or returns the second entry (thanks to the ResumeToken that points to the first entry)

        ChangeStreamDocument<BsonDocument> lastChangeStreamDocument = enumerator.Current;
        //lastChangeStreamDocument is now pointing to the second entry in the oplog, or the just received entry
        //A loop can be setup to call enumerator.MoveNext() to step through each entry in the oplog history and to also receive new events

        enumerator.Dispose();   //Be sure to dispose of the enumerator when finished.
    }
    catch( Exception ex)
    {
        //Logger.WriteException(ex);
    }
}

コードの改善について誰かが提案を持っている場合は、提案を提供してください。まだ勉強してる。

6
Jerren Saunders

上記の答えの多くは勇敢です(本当に驚くべきことです)...しかし、最終的にはそれらが壊れやすいのではないかと心配しています。

watch()メソッドのstartAtOperationTimeパラメーターを参照してください。これにより、特定の時点からコレクションの視聴を開始できます。私が知らないのは、MongoDBからサーバー時間を取得して設定する方法です。私には、このパラメーターをクライアント時間の値で使用することは意味がありません。

1
Robert

どういうわけか、oplog.rsコレクションの最新ドキュメントからresumeAfterトークンを作成することができました。

次のコードはNode.jsで記述されています。

const _ = require('lodash');
const { MongoClient } = require('mongodb');

localDB.collection('oplog.rs').findOne(
    {'ns': 'yourCollection'},
    {'sort': {'$natural': -1}},
    (err, doc) => {
        if (err || _.isEmpty(doc)) {
            someErrorCheck();
        }

        const resumeAfterData = [
            '82', // unknown
            doc.ts.toString(16), // timestamp
            '29', // unknown
            '29', // unknown
            '5A', // CType::BinData
            '10', // length (16)
            '04', // BinDataType of newUUID
            doc.ui.toString('hex'), // uuid
            '46', // CType::Object
            '64', // CType::OID (vary from the type of document primary key)
            '5F', // _ (vary from the field name of document primary key)
            '69', // i
            '64', // d
            '00', // null
            '64', // CType::OID (vary from the type of document primary key)
            _.get(doc, 'o2._id', 'o._id').toString('hex'), // ObjectId, update operations have `o2` field and others have `o` field
            '00', // null
            '04', // unknown
        ].join('').toUpperCase();

        console.log(resumeAfterData);
    },
);

しかし、私はそれらの82292904が何を意味するのかまだわかりません。

resumeAfterトークンの形式に関連するいくつかのMongoDB構成。これが私が持っているものです。

db.adminCommand({getParameter: 1, featureCompatibilityVersion: 1});

{
    "featureCompatibilityVersion" : {
        "version" : "4.0"
    },
    "ok" : 1.0,
    "operationTime" : Timestamp(1546854769, 1)
}
1
Vinta

私はこのポストなしで行っていることができませんでした。

2020年ですが、それでもMongodb3.6で変更ストリームを処理する必要があります。

誰かがPythonでこれを試してみる場合は、私の貧弱なPython3コードをアップロードしてください。

def make_resume_token(oplog_doc):
    rt = b'\x82'
    rt += oplog_doc['ts'].time.to_bytes(4, byteorder='big') + oplog_doc['ts'].inc.to_bytes(4, byteorder='big')
    rt += b'\x46\x64\x5f\x69\x64\x00\x64'
    rt += bytes.fromhex(str(oplog_doc['o']['_id']))
    rt += b'\x00\x5a\x10\x04'
    rt += oplog_doc['ui'].bytes
    rt += b'\x04'

    return {'_data' : rt}

cursor = db['COLLECTION_NAME'].watch(resume_after=make_resume_token(oplog_doc))
0
Ted Kim