-
Notifications
You must be signed in to change notification settings - Fork 2.3k
graph/db: cross-version graph Store queries #11175
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
4b66417
9dc6e7a
c60d9e2
411e3f0
45e4dbb
c8f6343
5ba2018
cb9e6b8
da63500
d2caf97
08733f4
6462add
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,6 +127,8 @@ func (c *ChannelGraph) Start() error { | |
| log.Criticalf("Could not populate the "+ | ||
| "graph cache: %v", err) | ||
| } | ||
|
|
||
| c.checkPreferredMappings(ctx) | ||
| }() | ||
| } else { | ||
| if err := c.populateCache(ctx); err != nil { | ||
|
|
@@ -138,9 +140,37 @@ func (c *ChannelGraph) Start() error { | |
| c.wg.Add(1) | ||
| go c.handleTopologySubscriptions(ctx) | ||
|
|
||
| // The synchronous cache path has finished its full graph read. Run the | ||
| // detection-only integrity check in the background so it doesn't delay | ||
| // startup. The asynchronous path runs it after cache population above. | ||
| if !c.opts.asyncGraphCachePopulation { | ||
| c.wg.Add(1) | ||
| go func() { | ||
| defer c.wg.Done() | ||
|
|
||
| c.checkPreferredMappings(ctx) | ||
| }() | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // checkPreferredMappings asks stores with cross-version preferred mappings to | ||
| // verify their derived indexes. The check only logs failures and divergence. | ||
| func (c *ChannelGraph) checkPreferredMappings(ctx context.Context) { | ||
| checker, ok := c.db.(interface { | ||
| checkPreferredMappings(context.Context) error | ||
| }) | ||
| if !ok { | ||
| return | ||
| } | ||
|
|
||
| err := checker.checkPreferredMappings(ctx) | ||
| if err != nil && !errors.Is(err, context.Canceled) { | ||
| log.ErrorS(ctx, "Unable to check preferred graph mappings", err) | ||
| } | ||
| } | ||
|
|
||
| // Stop signals any active goroutines for a graceful closure. | ||
| func (c *ChannelGraph) Stop() error { | ||
| if !c.stopped.CompareAndSwap(false, true) { | ||
|
|
@@ -214,6 +244,18 @@ func (c *ChannelGraph) handleTopologySubscriptions(ctx context.Context) { | |
| } | ||
| } | ||
|
|
||
| // cacheableFeatures reports whether a node's feature vector may overwrite the | ||
| // vector the graph cache already holds for that node. The cache keys features | ||
| // by pub key alone, so an empty v2 vector would otherwise shadow a non-empty | ||
| // v1 one. This is the same rule the no-cache FetchNodeFeatures fallback | ||
| // applies, and every path that feeds the cache must apply it, otherwise the | ||
| // cache disagrees with itself before and after a restart. | ||
| func cacheableFeatures(v lnwire.GossipVersion, | ||
| features *lnwire.FeatureVector) bool { | ||
|
|
||
| return v != gossipV2 || !features.IsEmpty() | ||
| } | ||
|
|
||
| // populateCache loads the entire channel graph into the in-memory graph cache. | ||
| func (c *ChannelGraph) populateCache(ctx context.Context) error { | ||
| if c.cache == nil { | ||
|
|
@@ -238,12 +280,21 @@ func (c *ChannelGraph) populateCache(ctx context.Context) error { | |
| for _, v := range []lnwire.GossipVersion{ | ||
| gossipV1, gossipV2, | ||
| } { | ||
| // TODO(elle): If we have both v1 and v2 entries for the same | ||
| // node/channel, prefer v2 when merging. | ||
| // We iterate v1 first, then v2. AddNodeFeatures overwrites on | ||
| // key collision, so v2 features take precedence when both | ||
| // versions exist, except for the entries that | ||
| // cacheableFeatures rejects. AddChannel ranks a colliding SCID | ||
| // itself, using the same rule as the preferred lookup tables, | ||
| // so the iteration order here does not decide which version a | ||
| // channel is cached from. | ||
| err := c.db.ForEachNodeCacheable(ctx, v, | ||
| func(node route.Vertex, | ||
| features *lnwire.FeatureVector) error { | ||
|
|
||
| if !cacheableFeatures(v, features) { | ||
| return nil | ||
| } | ||
|
|
||
| cache.AddNodeFeatures(node, features) | ||
|
|
||
| return nil | ||
|
|
@@ -299,9 +350,8 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(ctx context.Context, | |
| return c.cache.graphCache.ForEachChannel(node, cb) | ||
| } | ||
|
|
||
| // TODO(elle): once the no-cache path needs to support | ||
| // pathfinding across gossip versions, this should iterate | ||
| // across all versions rather than defaulting to v1. | ||
| // The no-cache path only runs against the KV backend, which is | ||
| // v1-only. | ||
| return c.db.ForEachNodeDirectedChannel( | ||
| ctx, gossipV1, node, cb, reset, | ||
| ) | ||
|
|
@@ -320,13 +370,16 @@ func (c *ChannelGraph) FetchNodeFeatures(ctx context.Context, | |
| return c.cache.graphCache.GetFeatures(node), nil | ||
| } | ||
|
|
||
| return c.db.FetchNodeFeatures(ctx, lnwire.GossipVersion1, node) | ||
| // The no-cache path only runs against the KV backend, which is | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: |
||
| // v1-only. | ||
| return c.db.FetchNodeFeatures(ctx, gossipV1, node) | ||
| } | ||
|
|
||
| // GraphSession will provide the call-back with access to a NodeTraverser | ||
| // instance which can be used to perform queries against the channel graph. If | ||
| // the graph cache is not enabled, then the call-back will be provided with | ||
| // access to the graph via a consistent read-only transaction. | ||
| // access to the graph via a consistent read-only transaction; the no-cache | ||
| // path only runs against the KV backend, which is v1-only. | ||
| func (c *ChannelGraph) GraphSession(ctx context.Context, | ||
| cb func(graph NodeTraverser) error, reset func()) error { | ||
|
|
||
|
|
@@ -373,7 +426,7 @@ func (c *ChannelGraph) AddNode(ctx context.Context, | |
| return err | ||
| } | ||
|
|
||
| if c.cache != nil { | ||
| if c.cache != nil && cacheableFeatures(node.Version, node.Features) { | ||
| c.cache.applyUpdate(func(cache *GraphCache) { | ||
| cache.AddNodeFeatures( | ||
| node.PubKeyBytes, node.Features, | ||
|
|
@@ -679,13 +732,22 @@ func (c *ChannelGraph) HasV1Node(ctx context.Context, | |
| return c.db.HasV1Node(ctx, nodePub) | ||
| } | ||
|
|
||
| // ForEachChannel iterates through all channel edges stored within the graph. | ||
| // ForEachChannel iterates through all channel edges stored within the graph | ||
| // across all gossip versions. | ||
| func (c *ChannelGraph) ForEachChannel(ctx context.Context, | ||
| v lnwire.GossipVersion, cb func(*models.ChannelEdgeInfo, | ||
| *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) error, | ||
| cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, | ||
| *models.ChannelEdgePolicy) error, | ||
| reset func()) error { | ||
|
|
||
| return c.db.ForEachChannel(ctx, v, cb, reset) | ||
| return c.db.ForEachChannel(ctx, cb, reset) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: Promoting these off |
||
| } | ||
|
|
||
| // ForEachNode iterates through all stored vertices/nodes in the graph across | ||
| // all gossip versions. | ||
| func (c *ChannelGraph) ForEachNode(ctx context.Context, | ||
| cb func(*models.Node) error, reset func()) error { | ||
|
|
||
| return c.db.ForEachNode(ctx, cb, reset) | ||
| } | ||
|
|
||
| // DisabledChannelIDs returns the channel ids of disabled channels. | ||
|
|
@@ -817,26 +879,23 @@ func (c *ChannelGraph) FetchChanInfos(ctx context.Context, | |
| } | ||
|
|
||
| // FetchChannelEdgesByOutpoint attempts to lookup directed edges by funding | ||
| // outpoint. | ||
| // outpoint, returning the highest available gossip version. | ||
| func (c *ChannelGraph) FetchChannelEdgesByOutpoint(ctx context.Context, | ||
| op *wire.OutPoint) ( | ||
| *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, | ||
| *models.ChannelEdgePolicy, error) { | ||
|
|
||
| return c.db.FetchChannelEdgesByOutpoint( | ||
| ctx, lnwire.GossipVersion1, op, | ||
| ) | ||
| return c.db.FetchChannelEdgesByOutpointPreferred(ctx, op) | ||
| } | ||
|
|
||
| // FetchChannelEdgesByID attempts to lookup directed edges by channel ID. | ||
| // FetchChannelEdgesByID attempts to lookup directed edges by channel ID, | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fetch now spans gossip versions, but |
||
| // returning the highest available gossip version. | ||
| func (c *ChannelGraph) FetchChannelEdgesByID(ctx context.Context, | ||
| chanID uint64) ( | ||
| *models.ChannelEdgeInfo, *models.ChannelEdgePolicy, | ||
| *models.ChannelEdgePolicy, error) { | ||
|
|
||
| return c.db.FetchChannelEdgesByID( | ||
| ctx, lnwire.GossipVersion1, chanID, | ||
| ) | ||
| return c.db.FetchChannelEdgesByIDPreferred(ctx, chanID) | ||
| } | ||
|
|
||
| // PutClosedScid stores a SCID for a closed channel in the database. | ||
|
|
@@ -869,6 +928,14 @@ func (c *ChannelGraph) PruneTip(ctx context.Context) (*chainhash.Hash, | |
|
|
||
| // VersionedGraph is a wrapper around ChannelGraph that will call underlying | ||
| // Store methods with a specific gossip version. | ||
| // | ||
| // NOTE: ForEachNode and ForEachChannel are the exception. They are promoted | ||
| // from the embedded ChannelGraph and iterate every gossip version, yielding | ||
| // the preferred record per pub key or SCID. Both callers that reach them | ||
| // through this wrapper want exactly that: autopilot ranks every candidate node | ||
| // for channel opening, and the discovery bootstrapper dials every known | ||
| // address, so neither has a reason to ignore a v2 announcement. A caller that | ||
| // does need one version must use a version-taking Store method instead. | ||
| type VersionedGraph struct { | ||
| *ChannelGraph | ||
| v lnwire.GossipVersion | ||
|
|
@@ -926,13 +993,6 @@ func (c *VersionedGraph) ForEachNodeCached(ctx context.Context, | |
| return c.ChannelGraph.ForEachNodeCached(ctx, c.v, cb, reset) | ||
| } | ||
|
|
||
| // ForEachNode iterates through all stored vertices/nodes in the graph. | ||
| func (c *VersionedGraph) ForEachNode(ctx context.Context, | ||
| cb func(*models.Node) error, reset func()) error { | ||
|
|
||
| return c.db.ForEachNode(ctx, c.v, cb, reset) | ||
| } | ||
|
|
||
| // NumZombies returns the current number of zombie channels in the graph. | ||
| func (c *VersionedGraph) NumZombies(ctx context.Context) (uint64, error) { | ||
| return c.db.NumZombies(ctx, c.v) | ||
|
|
@@ -964,24 +1024,6 @@ func (c *VersionedGraph) ChannelView(ctx context.Context) ([]EdgePoint, | |
| return c.db.ChannelView(ctx, c.v) | ||
| } | ||
|
|
||
| // GraphSession provides the callback with access to a NodeTraverser instance | ||
| // for performing queries against the channel graph. If the graph cache is | ||
| // enabled, the callback receives the VersionedGraph directly (which implements | ||
| // NodeTraverser using the cache). Otherwise a read-only database session is | ||
| // used. | ||
| func (c *VersionedGraph) GraphSession(ctx context.Context, | ||
| cb func(graph NodeTraverser) error, reset func()) error { | ||
|
|
||
| if c.cache != nil && c.cache.isLoaded() { | ||
| return cb(c) | ||
| } | ||
|
|
||
| // TODO(elle): the underlying GraphSession currently creates a | ||
| // NodeTraverser that is hardcoded to GossipVersion1. This needs to be | ||
| // updated to pass the version through for v2 support. | ||
| return c.db.GraphSession(ctx, cb, reset) | ||
| } | ||
|
|
||
| // FetchNode attempts to look up a target node by its identity public key. | ||
| func (c *VersionedGraph) FetchNode(ctx context.Context, | ||
| nodePub route.Vertex) (*models.Node, error) { | ||
|
|
@@ -1106,14 +1148,6 @@ func (c *VersionedGraph) ForEachNodeChannel(ctx context.Context, | |
| return c.db.ForEachNodeChannel(ctx, c.v, nodePub, cb, reset) | ||
| } | ||
|
|
||
| // ForEachChannel iterates through all channel edges stored within the graph. | ||
| func (c *VersionedGraph) ForEachChannel(ctx context.Context, | ||
| cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, | ||
| *models.ChannelEdgePolicy) error, reset func()) error { | ||
|
|
||
| return c.db.ForEachChannel(ctx, c.v, cb, reset) | ||
| } | ||
|
|
||
| // ForEachNodeCacheable iterates through all stored vertices/nodes in the graph. | ||
| func (c *VersionedGraph) ForEachNodeCacheable(ctx context.Context, | ||
| cb func(route.Vertex, *lnwire.FeatureVector) error, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Whole-graph iteration moves from a single-table cursor scan to a join through the new mapping tables, and every node, shell node, channel and policy write now issues an extra upsert that multiplies during initial gossip sync. Could you post before and after
BenchmarkGraphReadMethodsnumbers forForEachNodeandForEachChannel?