Neo4j Bolt Driver for Go: Code Reading - Routing Table

本記事では、Neo4j Driver として公式サポートされている、neo4j-go-driver における Routing Table の実装について見ていきます。

本記事では、v4.3 branch の最新である 5a14c7024ca commit hash のソースコードを前提にしています。

Routing Table

Routing Table の概要については、以下の記事を以前書きましたので、そちらをぜひご覧ください。

"Neo4j Bolt Protocol - Routing Table"

ROUTE Message Type Support

データベースインスタンスに対して Routing Table の情報をリクエストするのに ROUTE リクエストメッセージが策定されたのは以前の記事でも述べました。この ROUTE リクエストメッセージを利用したルーティングの実装は、以下の commit で実装されています。

fd5844fcf0f730ad3d4550de63e02709189f384b

ROUTE Message Request

Bolt Protocol の仕様 にある通り、ROUTE メッセージであることを示すフラグは、66 が利用されています。

const (
    //...
    msgRoute      byte = 0x66 // > 4.2
)

ROUTE メッセージのフィールドとしては、以下の三つでした。

routing::Dictionary,
bookmarks::List<String>,
db:String

この仕様に沿ったバイナリのエンコーディングは、outgoing.go において実装されています。routing/bookmarks/db それぞれのフィールドを順番にバイナリに変換していることが分かります。

// neo4j/internal/bolt/outgoing.go
func (o *outgoing) appendRoute(context map[string]string, bookmarks []string, database string) {
    //...
    o.begin()
    // signature
    o.packer.StructHeader(byte(msgRoute), 3)
    // routing
    o.packer.MapHeader(len(context))
    for k, v := range context {
        o.packer.String(k)
        o.packer.String(v)
    }
    // bookmarks
    o.packer.ArrayHeader(len(bookmarks))
    for _, bookmark := range bookmarks {
        o.packer.String(bookmark)
    }
    // database
    if database == db.DefaultDatabase {
        o.packer.Nil()
    } else {
        o.packer.String(database)
    }
    o.end()
}

ここで多用されている (p *Packer) String() は、PackStream で策定されている String 型のエンコーディング方式です。

Marker Size Maximum number of bytes
D0 8-bit big-endian unsigned integer 255 bytes
// neo4j/internal/packstream/packer.go
func (p *Packer) String(s string) {
    p.listHeader(len(s), 0x80, 0xd0)
    p.buf = append(p.buf, []byte(s)...)
}

ROUTE Message Response

hydrator.go の以下の箇所において、ROUTE リクエストメッセージを送った結果返却されたバイナリを、db.RoutingTable にデコードしています。

func (h *hydrator) routingTable() *db.RoutingTable {
    rt := db.RoutingTable{}
    nkeys := h.unp.Len()
    for ; nkeys > 0; nkeys-- {
        h.unp.Next()
        key := h.unp.String()
        h.unp.Next()
        switch key {
        case "ttl":
            rt.TimeToLive = int(h.unp.Int())
        case "servers":
            nservers := h.unp.Len()
            for ; nservers > 0; nservers-- {
                h.routingTableRole(&rt)
            }
        //...
        }
    }
    return &rt
}

func (h *hydrator) routingTableRole(rt *db.RoutingTable) {
    //...
}

db.RoutingTable は、TTL およびロール別のサーバーのアドレスを格納する slice をフィールドに持つ以下の構造体です。

type RoutingTable struct {
    TimeToLive int
    Routers    []string
    Readers    []string
    Writers    []string
}

Read Routing Table

Routing Table を読む実装自体は、readtable.go に実装されています。コネクションプールから利用できるコネクションを借りながら、ROUTE リクエストメッセージを送信し、データベースインスタンスから Routing Table 情報を取得しています。

// neo4j/internal/router/readtable.go

// Tries to read routing table from any of the specified routers using new or existing connection
// from the supplied pool.
func readTable(ctx context.Context, pool Pool, routers []string, routerContext map[string]string, bookmarks []string,
    database string, boltLogger log.BoltLogger) (*db.RoutingTable, error) {
    //...
}

最後に

以上、neo4j-go-driver の Routing Table の実装についての紹介を行いました。

2021-11-09