Repository: lightningnetwork/lnd
Pull request #9513 (2025-02-13 07:59:01)

graph+routing: refactor to remove `graphsession` 
With this PR, we completely remove the need for the `graphsession` package. 
With this refactor, we can also un-export 2 `ChannelGraph` methods which currently take
an optional `kvdb.RTx`. Un-exporting these ensures that we are not leaking the underlying DB 
implementation to callers and so will make graph DB abstraction easier. 

Part of https://github.com/lightningnetwork/lnd/issues/9494

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are limited to specific labels.
> 
> <details>
> <summary>:label: Labels to auto review (1)</summary>
> 
> * llm-review
> 
> </details>
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR. ([Beta](https://docs.coderabbit.ai/early-access#beta))
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

Patch:
diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md
index acd47780b..b664800fd 100644
--- a/docs/release-notes/release-notes-0.19.0.md
+++ b/docs/release-notes/release-notes-0.19.0.md
@@ -248,13 +248,16 @@ The underlying functionality between those two options remain the same.
 * Graph abstraction work:
     - [Abstract autopilot access](https://github.com/lightningnetwork/lnd/pull/9480)
     - [Abstract invoicerpc server access](https://github.com/lightningnetwork/lnd/pull/9516)
+    - [Refactor to hide DB transactions](https://github.com/lightningnetwork/lnd/pull/9513)
+
+* [Golang was updated to
+  `v1.22.11`](https://github.com/lightningnetwork/lnd/pull/9462). 
 
 * Move funding transaction validation to the gossiper
    [1](https://github.com/lightningnetwork/lnd/pull/9476)
    [2](https://github.com/lightningnetwork/lnd/pull/9477)
    [3](https://github.com/lightningnetwork/lnd/pull/9478).
 
-
 ## Breaking Changes
 ## Performance Improvements
 
diff --git a/graph/db/graph.go b/graph/db/graph.go
index 8004a2a5d..8ee9e86fd 100644
--- a/graph/db/graph.go
+++ b/graph/db/graph.go
@@ -403,16 +403,6 @@ func initChannelGraph(db kvdb.Backend) error {
 	return nil
 }
 
-// NewPathFindTx returns a new read transaction that can be used for a single
-// path finding session. Will return nil if the graph cache is enabled.
-func (c *ChannelGraph) NewPathFindTx() (kvdb.RTx, error) {
-	if c.graphCache != nil {
-		return nil, nil
-	}
-
-	return c.db.BeginReadTx()
-}
-
 // AddrsForNode returns all known addresses for the target node public key that
 // the graph DB is aware of. The returned boolean indicates if the given node is
 // unknown to the graph DB or not.
@@ -500,13 +490,14 @@ func (c *ChannelGraph) ForEachChannel(cb func(*models.ChannelEdgeInfo,
 	}, func() {})
 }
 
-// ForEachNodeDirectedChannel iterates through all channels of a given node,
+// forEachNodeDirectedChannel iterates through all channels of a given node,
 // executing the passed callback on the directed edge representing the channel
 // and its incoming policy. If the callback returns an error, then the iteration
-// is halted with the error propagated back up to the caller.
+// is halted with the error propagated back up to the caller. An optional read
+// transaction may be provided. If none is provided, a new one will be created.
 //
 // Unknown policies are passed into the callback as nil values.
-func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
+func (c *ChannelGraph) forEachNodeDirectedChannel(tx kvdb.RTx,
 	node route.Vertex, cb func(channel *DirectedChannel) error) error {
 
 	if c.graphCache != nil {
@@ -517,7 +508,7 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
 	toNodeCallback := func() route.Vertex {
 		return node
 	}
-	toNodeFeatures, err := c.FetchNodeFeatures(node)
+	toNodeFeatures, err := c.fetchNodeFeatures(tx, node)
 	if err != nil {
 		return err
 	}
@@ -561,9 +552,10 @@ func (c *ChannelGraph) ForEachNodeDirectedChannel(tx kvdb.RTx,
 	return nodeTraversal(tx, node[:], c.db, dbCallback)
 }
 
-// FetchNodeFeatures returns the features of a given node. If no features are
-// known for the node, an empty feature vector is returned.
-func (c *ChannelGraph) FetchNodeFeatures(
+// fetchNodeFeatures returns the features of a given node. If no features are
+// known for the node, an empty feature vector is returned. An optional read
+// transaction may be provided. If none is provided, a new one will be created.
+func (c *ChannelGraph) fetchNodeFeatures(tx kvdb.RTx,
 	node route.Vertex) (*lnwire.FeatureVector, error) {
 
 	if c.graphCache != nil {
@@ -571,7 +563,7 @@ func (c *ChannelGraph) FetchNodeFeatures(
 	}
 
 	// Fallback that uses the database.
-	targetNode, err := c.FetchLightningNode(node)
+	targetNode, err := c.FetchLightningNodeTx(tx, node)
 	switch err {
 	// If the node exists and has features, return them directly.
 	case nil:
@@ -588,6 +580,34 @@ func (c *ChannelGraph) FetchNodeFeatures(
 	}
 }
 
+// ForEachNodeDirectedChannel iterates through all channels of a given node,
+// executing the passed callback on the directed edge representing the channel
+// and its incoming policy. If the callback returns an error, then the iteration
+// is halted with the error propagated back up to the caller. If the graphCache
+// is available, then it will be used to retrieve the node's channels instead
+// of the database.
+//
+// Unknown policies are passed into the callback as nil values.
+//
+// NOTE: this is part of the graphdb.NodeTraverser interface.
+func (c *ChannelGraph) ForEachNodeDirectedChannel(nodePub route.Vertex,
+	cb func(channel *DirectedChannel) error) error {
+
+	return c.forEachNodeDirectedChannel(nil, nodePub, cb)
+}
+
+// FetchNodeFeatures returns the features of the given node. If no features are
+// known for the node, an empty feature vector is returned.
+// If the graphCache is available, then it will be used to retrieve the node's
+// features instead of the database.
+//
+// NOTE: this is part of the graphdb.NodeTraverser interface.
+func (c *ChannelGraph) FetchNodeFeatures(nodePub route.Vertex) (
+	*lnwire.FeatureVector, error) {
+
+	return c.fetchNodeFeatures(nil, nodePub)
+}
+
 // ForEachNodeCached is similar to forEachNode, but it utilizes the channel
 // graph cache instead. Note that this doesn't return all the information the
 // regular forEachNode method does.
@@ -617,8 +637,8 @@ func (c *ChannelGraph) ForEachNodeCached(cb func(node route.Vertex,
 				toNodeCallback := func() route.Vertex {
 					return node.PubKeyBytes
 				}
-				toNodeFeatures, err := c.FetchNodeFeatures(
-					node.PubKeyBytes,
+				toNodeFeatures, err := c.fetchNodeFeatures(
+					tx, node.PubKeyBytes,
 				)
 				if err != nil {
 					return err
@@ -3873,6 +3893,64 @@ func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) {
 	return isClosed, nil
 }
 
+// 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.
+func (c *ChannelGraph) GraphSession(cb func(graph NodeTraverser) error) error {
+	var (
+		tx     kvdb.RTx
+		err    error
+		commit = func() {}
+	)
+	if c.graphCache == nil {
+		tx, err = c.db.BeginReadTx()
+		if err != nil {
+			return err
+		}
+
+		commit = func() {
+			if err := tx.Rollback(); err != nil {
+				log.Errorf("Unable to rollback tx: %v", err)
+			}
+		}
+	}
+	defer commit()
+
+	return cb(&nodeTraverserSession{
+		db: c,
+		tx: tx,
+	})
+}
+
+// nodeTraverserSession implements the NodeTraverser interface but with a
+// backing read only transaction for a consistent view of the graph in the case
+// where the graph Cache has not been enabled.
+type nodeTraverserSession struct {
+	tx kvdb.RTx
+	db *ChannelGraph
+}
+
+// ForEachNodeDirectedChannel calls the callback for every channel of the given
+// node.
+//
+// NOTE: Part of the NodeTraverser interface.
+func (c *nodeTraverserSession) ForEachNodeDirectedChannel(nodePub route.Vertex,
+	cb func(channel *DirectedChannel) error) error {
+
+	return c.db.forEachNodeDirectedChannel(c.tx, nodePub, cb)
+}
+
+// FetchNodeFeatures returns the features of the given node. If the node is
+// unknown, assume no additional features are supported.
+//
+// NOTE: Part of the NodeTraverser interface.
+func (c *nodeTraverserSession) FetchNodeFeatures(nodePub route.Vertex) (
+	*lnwire.FeatureVector, error) {
+
+	return c.db.fetchNodeFeatures(c.tx, nodePub)
+}
+
 func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // nolint:dupl
 	updateIndex kvdb.RwBucket, node *models.LightningNode) error {
 
diff --git a/graph/db/interfaces.go b/graph/db/interfaces.go
index f44a9ff8b..f5dce71ca 100644
--- a/graph/db/interfaces.go
+++ b/graph/db/interfaces.go
@@ -2,6 +2,7 @@ package graphdb
 
 import (
 	"github.com/lightningnetwork/lnd/graph/db/models"
+	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
 )
 
@@ -23,3 +24,16 @@ type NodeRTx interface {
 	// the same transaction.
 	FetchNode(node route.Vertex) (NodeRTx, error)
 }
+
+// NodeTraverser is an abstract read only interface that provides information
+// about nodes and their edges. The interface is about providing fast read-only
+// access to the graph and so if a cache is available, it should be used.
+type NodeTraverser interface {
+	// ForEachNodeDirectedChannel calls the callback for every channel of
+	// the given node.
+	ForEachNodeDirectedChannel(nodePub route.Vertex,
+		cb func(channel *DirectedChannel) error) error
+
+	// FetchNodeFeatures returns the features of the given node.
+	FetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error)
+}
diff --git a/graph/graphsession/graph_session.go b/graph/graphsession/graph_session.go
deleted file mode 100644
index 6976fad79..000000000
--- a/graph/graphsession/graph_session.go
+++ /dev/null
@@ -1,141 +0,0 @@
-package graphsession
-
-import (
-	"fmt"
-
-	graphdb "github.com/lightningnetwork/lnd/graph/db"
-	"github.com/lightningnetwork/lnd/kvdb"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/lightningnetwork/lnd/routing"
-	"github.com/lightningnetwork/lnd/routing/route"
-)
-
-// Factory implements the routing.GraphSessionFactory and can be used to start
-// a session with a ReadOnlyGraph.
-type Factory struct {
-	graph ReadOnlyGraph
-}
-
-// NewGraphSessionFactory constructs a new Factory which can then be used to
-// start a new session.
-func NewGraphSessionFactory(graph ReadOnlyGraph) routing.GraphSessionFactory {
-	return &Factory{
-		graph: graph,
-	}
-}
-
-// NewGraphSession will produce a new Graph to use for a path-finding session.
-// It returns the Graph along with a call-back that must be called once Graph
-// access is complete. This call-back will close any read-only transaction that
-// was created at Graph construction time.
-//
-// NOTE: This is part of the routing.GraphSessionFactory interface.
-func (g *Factory) NewGraphSession() (routing.Graph, func() error, error) {
-	tx, err := g.graph.NewPathFindTx()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	session := &session{
-		graph: g.graph,
-		tx:    tx,
-	}
-
-	return session, session.close, nil
-}
-
-// A compile-time check to ensure that Factory implements the
-// routing.GraphSessionFactory interface.
-var _ routing.GraphSessionFactory = (*Factory)(nil)
-
-// session is an implementation of the routing.Graph interface where the same
-// read-only transaction is held across calls to the graph and can be used to
-// access the backing channel graph.
-type session struct {
-	graph graph
-	tx    kvdb.RTx
-}
-
-// NewRoutingGraph constructs a session that which does not first start a
-// read-only transaction and so each call on the routing.Graph will create a
-// new transaction.
-func NewRoutingGraph(graph ReadOnlyGraph) routing.Graph {
-	return &session{
-		graph: graph,
-	}
-}
-
-// close closes the read-only transaction being used to access the backing
-// graph. If no transaction was started then this is a no-op.
-func (g *session) close() error {
-	if g.tx == nil {
-		return nil
-	}
-
-	err := g.tx.Rollback()
-	if err != nil {
-		return fmt.Errorf("error closing db tx: %w", err)
-	}
-
-	return nil
-}
-
-// ForEachNodeChannel calls the callback for every channel of the given node.
-//
-// NOTE: Part of the routing.Graph interface.
-func (g *session) ForEachNodeChannel(nodePub route.Vertex,
-	cb func(channel *graphdb.DirectedChannel) error) error {
-
-	return g.graph.ForEachNodeDirectedChannel(g.tx, nodePub, cb)
-}
-
-// FetchNodeFeatures returns the features of the given node. If the node is
-// unknown, assume no additional features are supported.
-//
-// NOTE: Part of the routing.Graph interface.
-func (g *session) FetchNodeFeatures(nodePub route.Vertex) (
-	*lnwire.FeatureVector, error) {
-
-	return g.graph.FetchNodeFeatures(nodePub)
-}
-
-// A compile-time check to ensure that *session implements the
-// routing.Graph interface.
-var _ routing.Graph = (*session)(nil)
-
-// ReadOnlyGraph is a graph extended with a call to create a new read-only
-// transaction that can then be used to make further queries to the graph.
-type ReadOnlyGraph interface {
-	// NewPathFindTx returns a new read transaction that can be used for a
-	// single path finding session. Will return nil if the graph cache is
-	// enabled.
-	NewPathFindTx() (kvdb.RTx, error)
-
-	graph
-}
-
-// graph describes the API necessary for a graph source to have access to on a
-// database implementation, like channeldb.ChannelGraph, in order to be used by
-// the Router for pathfinding.
-type graph interface {
-	// ForEachNodeDirectedChannel iterates through all channels of a given
-	// node, executing the passed callback on the directed edge representing
-	// the channel and its incoming policy. If the callback returns an
-	// error, then the iteration is halted with the error propagated back
-	// up to the caller.
-	//
-	// Unknown policies are passed into the callback as nil values.
-	//
-	// NOTE: if a nil tx is provided, then it is expected that the
-	// implementation create a read only tx.
-	ForEachNodeDirectedChannel(tx kvdb.RTx, node route.Vertex,
-		cb func(channel *graphdb.DirectedChannel) error) error
-
-	// FetchNodeFeatures returns the features of a given node. If no
-	// features are known for the node, an empty feature vector is returned.
-	FetchNodeFeatures(node route.Vertex) (*lnwire.FeatureVector, error)
-}
-
-// A compile-time check to ensure that *channeldb.ChannelGraph implements the
-// graph interface.
-var _ graph = (*graphdb.ChannelGraph)(nil)
diff --git a/routing/bandwidth.go b/routing/bandwidth.go
index a552628c7..833782616 100644
--- a/routing/bandwidth.go
+++ b/routing/bandwidth.go
@@ -63,7 +63,7 @@ func newBandwidthManager(graph Graph, sourceNode route.Vertex,
 
 	// First, we'll collect the set of outbound edges from the target
 	// source node and add them to our bandwidth manager's map of channels.
-	err := graph.ForEachNodeChannel(sourceNode,
+	err := graph.ForEachNodeDirectedChannel(sourceNode,
 		func(channel *graphdb.DirectedChannel) error {
 			shortID := lnwire.NewShortChanIDFromInt(
 				channel.ChannelID,
diff --git a/routing/graph.go b/routing/graph.go
index 7608ee92b..115f2daf1 100644
--- a/routing/graph.go
+++ b/routing/graph.go
@@ -12,25 +12,24 @@ import (
 // Graph is an abstract interface that provides information about nodes and
 // edges to pathfinding.
 type Graph interface {
-	// ForEachNodeChannel calls the callback for every channel of the given
-	// node.
-	ForEachNodeChannel(nodePub route.Vertex,
+	// ForEachNodeDirectedChannel calls the callback for every channel of
+	// the given node.
+	ForEachNodeDirectedChannel(nodePub route.Vertex,
 		cb func(channel *graphdb.DirectedChannel) error) error
 
 	// FetchNodeFeatures returns the features of the given node.
 	FetchNodeFeatures(nodePub route.Vertex) (*lnwire.FeatureVector, error)
 }
 
-// GraphSessionFactory can be used to produce a new Graph instance which can
-// then be used for a path-finding session. Depending on the implementation,
-// the Graph session will represent a DB connection where a read-lock is being
-// held across calls to the backing Graph.
+// GraphSessionFactory can be used to gain access to a graphdb.NodeTraverser
+// instance which can then be used for a path-finding session. Depending on the
+// implementation, the session will represent a DB connection where a read-lock
+// is being held across calls to the backing graph.
 type GraphSessionFactory interface {
-	// NewGraphSession will produce a new Graph to use for a path-finding
-	// session. It returns the Graph along with a call-back that must be
-	// called once Graph access is complete. This call-back will close any
-	// read-only transaction that was created at Graph construction time.
-	NewGraphSession() (Graph, func() error, error)
+	// GraphSession will provide the call-back with access to a
+	// graphdb.NodeTraverser instance which can be used to perform queries
+	// against the channel graph.
+	GraphSession(cb func(graph graphdb.NodeTraverser) error) error
 }
 
 // FetchAmountPairCapacity determines the maximal public capacity between two
diff --git a/routing/pathfind.go b/routing/pathfind.go
index 3f1d0ba09..c14e32e8b 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -557,7 +557,7 @@ func getOutgoingBalance(node route.Vertex, outgoingChans map[uint64]struct{},
 	}
 
 	// Iterate over all channels of the to node.
-	err := g.ForEachNodeChannel(node, cb)
+	err := g.ForEachNodeDirectedChannel(node, cb)
 	if err != nil {
 		return 0, 0, err
 	}
@@ -1325,7 +1325,7 @@ func processNodeForBlindedPath(g Graph, node route.Vertex,
 
 	// Now, iterate over the node's channels in search for paths to this
 	// node that can be used for blinded paths
-	err = g.ForEachNodeChannel(node,
+	err = g.ForEachNodeDirectedChannel(node,
 		func(channel *graphdb.DirectedChannel) error {
 			// Keep track of how many incoming channels this node
 			// has. We only use a node as an introduction node if it
diff --git a/routing/payment_session.go b/routing/payment_session.go
index 0afdf822f..607c10443 100644
--- a/routing/payment_session.go
+++ b/routing/payment_session.go
@@ -6,6 +6,7 @@ import (
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btclog/v2"
 	"github.com/lightningnetwork/lnd/channeldb"
+	graphdb "github.com/lightningnetwork/lnd/graph/db"
 	"github.com/lightningnetwork/lnd/graph/db/models"
 	"github.com/lightningnetwork/lnd/lnutils"
 	"github.com/lightningnetwork/lnd/lnwire"
@@ -235,6 +236,17 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
 	}, nil
 }
 
+// pathFindingError is a wrapper error type that is used to distinguish path
+// finding errors from other errors in path finding loop.
+type pathFindingError struct {
+	error
+}
+
+// Unwrap returns the underlying error.
+func (e *pathFindingError) Unwrap() error {
+	return e.error
+}
+
 // RequestRoute returns a route which is likely to be capable for successfully
 // routing the specified HTLC payment to the target node. Initially the first
 // set of paths returned from this method may encounter routing failure along
@@ -295,13 +307,8 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 		maxAmt = *p.payment.MaxShardAmt
 	}
 
-	for {
-		// Get a routing graph session.
-		graph, closeGraph, err := p.graphSessFactory.NewGraphSession()
-		if err != nil {
-			return nil, err
-		}
-
+	var path []*unifiedEdge
+	findPath := func(graph graphdb.NodeTraverser) error {
 		// We'll also obtain a set of bandwidthHints from the lower
 		// layer for each of our outbound channels. This will allow the
 		// path finding to skip any links that aren't active or just
@@ -310,19 +317,13 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 		// attempt, because concurrent payments may change balances.
 		bandwidthHints, err := p.getBandwidthHints(graph)
 		if err != nil {
-			// Close routing graph session.
-			if graphErr := closeGraph(); graphErr != nil {
-				log.Errorf("could not close graph session: %v",
-					graphErr)
-			}
-
-			return nil, err
+			return err
 		}
 
 		p.log.Debugf("pathfinding for amt=%v", maxAmt)
 
 		// Find a route for the current amount.
-		path, _, err := p.pathFinder(
+		path, _, err = p.pathFinder(
 			&graphParams{
 				additionalEdges: p.additionalEdges,
 				bandwidthHints:  bandwidthHints,
@@ -332,12 +333,31 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 			p.selfNode, p.selfNode, p.payment.Target,
 			maxAmt, p.payment.TimePref, finalHtlcExpiry,
 		)
+		if err != nil {
+			// Wrap the error to distinguish path finding errors
+			// from other errors in this closure.
+			return &pathFindingError{err}
+		}
 
-		// Close routing graph session.
-		if err := closeGraph(); err != nil {
-			log.Errorf("could not close graph session: %v", err)
+		return nil
+	}
+
+	for {
+		err := p.graphSessFactory.GraphSession(findPath)
+		// If there is an error, and it is not a path finding error, we
+		// return it immediately.
+		if err != nil && !lnutils.ErrorAs[*pathFindingError](err) {
+			return nil, err
+		} else if err != nil {
+			// If the error is a path finding error, we'll unwrap it
+			// to check the underlying error.
+			//
+			//nolint:errorlint
+			pErr, _ := err.(*pathFindingError)
+			err = pErr.Unwrap()
 		}
 
+		// Otherwise, we'll switch on the path finding error.
 		switch {
 		case err == errNoPathFound:
 			// Don't split if this is a legacy payment without mpp
diff --git a/routing/unified_edges.go b/routing/unified_edges.go
index b92010da0..a18547485 100644
--- a/routing/unified_edges.go
+++ b/routing/unified_edges.go
@@ -125,7 +125,7 @@ func (u *nodeEdgeUnifier) addGraphPolicies(g Graph) error {
 	}
 
 	// Iterate over all channels of the to node.
-	return g.ForEachNodeChannel(u.toNode, cb)
+	return g.ForEachNodeDirectedChannel(u.toNode, cb)
 }
 
 // unifiedEdge is the individual channel data that is kept inside an edgeUnifier
diff --git a/rpcserver.go b/rpcserver.go
index e096d31f8..2ed695ff0 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -51,7 +51,6 @@ import (
 	"github.com/lightningnetwork/lnd/graph"
 	graphdb "github.com/lightningnetwork/lnd/graph/db"
 	"github.com/lightningnetwork/lnd/graph/db/models"
-	"github.com/lightningnetwork/lnd/graph/graphsession"
 	"github.com/lightningnetwork/lnd/htlcswitch"
 	"github.com/lightningnetwork/lnd/htlcswitch/hop"
 	"github.com/lightningnetwork/lnd/input"
@@ -711,8 +710,8 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
 			amount lnwire.MilliSatoshi) (btcutil.Amount, error) {
 
 			return routing.FetchAmountPairCapacity(
-				graphsession.NewRoutingGraph(graph),
-				selfNode.PubKeyBytes, nodeFrom, nodeTo, amount,
+				graph, selfNode.PubKeyBytes, nodeFrom, nodeTo,
+				amount,
 			)
 		},
 		FetchChannelEndpoints: func(chanID uint64) (route.Vertex,
diff --git a/server.go b/server.go
index f85d60f52..ecb3eceae 100644
--- a/server.go
+++ b/server.go
@@ -45,7 +45,6 @@ import (
 	"github.com/lightningnetwork/lnd/graph"
 	graphdb "github.com/lightningnetwork/lnd/graph/db"
 	"github.com/lightningnetwork/lnd/graph/db/models"
-	"github.com/lightningnetwork/lnd/graph/graphsession"
 	"github.com/lightningnetwork/lnd/healthcheck"
 	"github.com/lightningnetwork/lnd/htlcswitch"
 	"github.com/lightningnetwork/lnd/htlcswitch/hop"
@@ -1038,13 +1037,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 		return nil, fmt.Errorf("error getting source node: %w", err)
 	}
 	paymentSessionSource := &routing.SessionSource{
-		GraphSessionFactory: graphsession.NewGraphSessionFactory(
-			dbs.GraphDB,
-		),
-		SourceNode:        sourceNode,
-		MissionControl:    s.defaultMC,
-		GetLink:           s.htlcSwitch.GetLinkByShortID,
-		PathFindingConfig: pathFindingConfig,
+		GraphSessionFactory: dbs.GraphDB,
+		SourceNode:          sourceNode,
+		MissionControl:      s.defaultMC,
+		GetLink:             s.htlcSwitch.GetLinkByShortID,
+		PathFindingConfig:   pathFindingConfig,
 	}
 
 	paymentControl := channeldb.NewPaymentControl(dbs.ChanStateDB)
@@ -1073,7 +1070,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 
 	s.chanRouter, err = routing.New(routing.Config{
 		SelfNode:           selfNode.PubKeyBytes,
-		RoutingGraph:       graphsession.NewRoutingGraph(dbs.GraphDB),
+		RoutingGraph:       dbs.GraphDB,
 		Chain:              cc.ChainIO,
 		Payer:              s.htlcSwitch,
 		Control:            s.controlTower,


Test patch:
diff --git a/graph/db/graph_test.go b/graph/db/graph_test.go
index eb658e2f1..62b9cd4e1 100644
--- a/graph/db/graph_test.go
+++ b/graph/db/graph_test.go
@@ -3915,7 +3915,7 @@ func BenchmarkForEachChannel(b *testing.B) {
 	}
 }
 
-// TestGraphCacheForEachNodeChannel tests that the ForEachNodeDirectedChannel
+// TestGraphCacheForEachNodeChannel tests that the forEachNodeDirectedChannel
 // method works as expected, and is able to handle nil self edges.
 func TestGraphCacheForEachNodeChannel(t *testing.T) {
 	graph, err := MakeTestGraph(t)
@@ -3952,7 +3952,7 @@ func TestGraphCacheForEachNodeChannel(t *testing.T) {
 
 	getSingleChannel := func() *DirectedChannel {
 		var ch *DirectedChannel
-		err = graph.ForEachNodeDirectedChannel(nil, node1.PubKeyBytes,
+		err = graph.forEachNodeDirectedChannel(nil, node1.PubKeyBytes,
 			func(c *DirectedChannel) error {
 				require.Nil(t, ch)
 				ch = c
diff --git a/routing/integrated_routing_context_test.go b/routing/integrated_routing_context_test.go
index e4241dac5..7270b6f6d 100644
--- a/routing/integrated_routing_context_test.go
+++ b/routing/integrated_routing_context_test.go
@@ -8,7 +8,6 @@ import (
 	"time"
 
 	"github.com/lightningnetwork/lnd/fn/v2"
-	graphdb "github.com/lightningnetwork/lnd/graph/db"
 	"github.com/lightningnetwork/lnd/kvdb"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
@@ -211,7 +210,7 @@ func (c *integratedRoutingContext) testPayment(maxParts uint32,
 
 	session, err := newPaymentSession(
 		&payment, c.graph.source.pubkey, getBandwidthHints,
-		newMockGraphSessionFactory(c.graph), mc, c.pathFindingCfg,
+		c.graph, mc, c.pathFindingCfg,
 	)
 	if err != nil {
 		c.t.Fatal(err)
@@ -317,88 +316,3 @@ func getNodeIndex(route *route.Route, failureSource route.Vertex) *int {
 	}
 	return nil
 }
-
-type mockGraphSessionFactory struct {
-	Graph
-}
-
-func newMockGraphSessionFactory(graph Graph) GraphSessionFactory {
-	return &mockGraphSessionFactory{Graph: graph}
-}
-
-func (m *mockGraphSessionFactory) NewGraphSession() (Graph, func() error,
-	error) {
-
-	return m, func() error {
-		return nil
-	}, nil
-}
-
-var _ GraphSessionFactory = (*mockGraphSessionFactory)(nil)
-var _ Graph = (*mockGraphSessionFactory)(nil)
-
-type mockGraphSessionFactoryChanDB struct {
-	graph *graphdb.ChannelGraph
-}
-
-func newMockGraphSessionFactoryFromChanDB(
-	graph *graphdb.ChannelGraph) *mockGraphSessionFactoryChanDB {
-
-	return &mockGraphSessionFactoryChanDB{
-		graph: graph,
-	}
-}
-
-func (g *mockGraphSessionFactoryChanDB) NewGraphSession() (Graph, func() error,
-	error) {
-
-	tx, err := g.graph.NewPathFindTx()
-	if err != nil {
-		return nil, nil, err
-	}
-
-	session := &mockGraphSessionChanDB{
-		graph: g.graph,
-		tx:    tx,
-	}
-
-	return session, session.close, nil
-}
-
-var _ GraphSessionFactory = (*mockGraphSessionFactoryChanDB)(nil)
-
-type mockGraphSessionChanDB struct {
-	graph *graphdb.ChannelGraph
-	tx    kvdb.RTx
-}
-
-func newMockGraphSessionChanDB(graph *graphdb.ChannelGraph) Graph {
-	return &mockGraphSessionChanDB{
-		graph: graph,
-	}
-}
-
-func (g *mockGraphSessionChanDB) close() error {
-	if g.tx == nil {
-		return nil
-	}
-
-	err := g.tx.Rollback()
-	if err != nil {
-		return fmt.Errorf("error closing db tx: %w", err)
-	}
-
-	return nil
-}
-
-func (g *mockGraphSessionChanDB) ForEachNodeChannel(nodePub route.Vertex,
-	cb func(channel *graphdb.DirectedChannel) error) error {
-
-	return g.graph.ForEachNodeDirectedChannel(g.tx, nodePub, cb)
-}
-
-func (g *mockGraphSessionChanDB) FetchNodeFeatures(nodePub route.Vertex) (
-	*lnwire.FeatureVector, error) {
-
-	return g.graph.FetchNodeFeatures(nodePub)
-}
diff --git a/routing/integrated_routing_test.go b/routing/integrated_routing_test.go
index 4a2447b48..9636b10f7 100644
--- a/routing/integrated_routing_test.go
+++ b/routing/integrated_routing_test.go
@@ -404,5 +404,5 @@ func TestPaymentAddrOnlyNoSplit(t *testing.T) {
 	// The payment should have failed since we need to split in order to
 	// route a payment to the destination, but they don't actually support
 	// MPP.
-	require.Equal(t, err.Error(), errNoPathFound.Error())
+	require.ErrorIs(t, err, errNoPathFound)
 }
diff --git a/routing/mock_graph_test.go b/routing/mock_graph_test.go
index cab7c9726..cb5d07f3a 100644
--- a/routing/mock_graph_test.go
+++ b/routing/mock_graph_test.go
@@ -165,7 +165,7 @@ func (m *mockGraph) addChannel(id uint64, node1id, node2id byte,
 // forEachNodeChannel calls the callback for every channel of the given node.
 //
 // NOTE: Part of the Graph interface.
-func (m *mockGraph) ForEachNodeChannel(nodePub route.Vertex,
+func (m *mockGraph) ForEachNodeDirectedChannel(nodePub route.Vertex,
 	cb func(channel *graphdb.DirectedChannel) error) error {
 
 	// Look up the mock node.
@@ -227,6 +227,17 @@ func (m *mockGraph) FetchNodeFeatures(nodePub route.Vertex) (
 	return lnwire.EmptyFeatureVector(), nil
 }
 
+// GraphSession will provide the call-back with access to a
+// graphdb.NodeTraverser instance which can be used to perform queries against
+// the channel graph.
+//
+// NOTE: Part of the GraphSessionFactory interface.
+func (m *mockGraph) GraphSession(
+	cb func(graph graphdb.NodeTraverser) error) error {
+
+	return cb(m)
+}
+
 // htlcResult describes the resolution of an htlc. If failure is nil, the htlc
 // was settled.
 type htlcResult struct {
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index f91d95d6b..fd92ed934 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -3221,30 +3221,25 @@ func dbFindPath(graph *graphdb.ChannelGraph,
 		return nil, err
 	}
 
-	graphSessFactory := newMockGraphSessionFactoryFromChanDB(graph)
+	var route []*unifiedEdge
+	err = graph.GraphSession(func(graph graphdb.NodeTraverser) error {
+		route, _, err = findPath(
+			&graphParams{
+				additionalEdges: additionalEdges,
+				bandwidthHints:  bandwidthHints,
+				graph:           graph,
+			},
+			r, cfg, sourceNode.PubKeyBytes, source, target, amt,
+			timePref, finalHtlcExpiry,
+		)
 
-	graphSess, closeGraphSess, err := graphSessFactory.NewGraphSession()
+		return err
+	})
 	if err != nil {
 		return nil, err
 	}
 
-	defer func() {
-		if err := closeGraphSess(); err != nil {
-			log.Errorf("Error closing graph session: %v", err)
-		}
-	}()
-
-	route, _, err := findPath(
-		&graphParams{
-			additionalEdges: additionalEdges,
-			bandwidthHints:  bandwidthHints,
-			graph:           graphSess,
-		},
-		r, cfg, sourceNode.PubKeyBytes, source, target, amt, timePref,
-		finalHtlcExpiry,
-	)
-
-	return route, err
+	return route, nil
 }
 
 // dbFindBlindedPaths calls findBlindedPaths after getting a db transaction from
@@ -3258,8 +3253,7 @@ func dbFindBlindedPaths(graph *graphdb.ChannelGraph,
 	}
 
 	return findBlindedPaths(
-		newMockGraphSessionChanDB(graph), sourceNode.PubKeyBytes,
-		restrictions,
+		graph, sourceNode.PubKeyBytes, restrictions,
 	)
 }
 
diff --git a/routing/payment_session_test.go b/routing/payment_session_test.go
index 278e09044..51eeda425 100644
--- a/routing/payment_session_test.go
+++ b/routing/payment_session_test.go
@@ -4,6 +4,7 @@ import (
 	"testing"
 	"time"
 
+	graphdb "github.com/lightningnetwork/lnd/graph/db"
 	"github.com/lightningnetwork/lnd/graph/db/models"
 	"github.com/lightningnetwork/lnd/lntypes"
 	"github.com/lightningnetwork/lnd/lnwire"
@@ -118,7 +119,7 @@ func TestUpdateAdditionalEdge(t *testing.T) {
 		func(Graph) (bandwidthHints, error) {
 			return &mockBandwidthHints{}, nil
 		},
-		newMockGraphSessionFactory(&sessionGraph{}),
+		&sessionGraph{},
 		&MissionControl{},
 		PathFindingConfig{},
 	)
@@ -196,7 +197,7 @@ func TestRequestRoute(t *testing.T) {
 		func(Graph) (bandwidthHints, error) {
 			return &mockBandwidthHints{}, nil
 		},
-		newMockGraphSessionFactory(&sessionGraph{}),
+		&sessionGraph{},
 		&MissionControl{},
 		PathFindingConfig{},
 	)
@@ -257,3 +258,9 @@ type sessionGraph struct {
 func (g *sessionGraph) sourceNode() route.Vertex {
 	return route.Vertex{}
 }
+
+func (g *sessionGraph) GraphSession(
+	cb func(graph graphdb.NodeTraverser) error) error {
+
+	return cb(g)
+}
diff --git a/routing/router_test.go b/routing/router_test.go
index 543f3b000..6efc75df2 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -135,20 +135,18 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
 	sourceNode, err := graphInstance.graph.SourceNode()
 	require.NoError(t, err)
 	sessionSource := &SessionSource{
-		GraphSessionFactory: newMockGraphSessionFactoryFromChanDB(
-			graphInstance.graph,
-		),
-		SourceNode:        sourceNode,
-		GetLink:           graphInstance.getLink,
-		PathFindingConfig: pathFindingConfig,
-		MissionControl:    mc,
+		GraphSessionFactory: graphInstance.graph,
+		SourceNode:          sourceNode,
+		GetLink:             graphInstance.getLink,
+		PathFindingConfig:   pathFindingConfig,
+		MissionControl:      mc,
 	}
 
 	graphBuilder := newMockGraphBuilder(graphInstance.graph)
 
 	router, err := New(Config{
 		SelfNode:       sourceNode.PubKeyBytes,
-		RoutingGraph:   newMockGraphSessionChanDB(graphInstance.graph),
+		RoutingGraph:   graphInstance.graph,
 		Chain:          chain,
 		Payer:          &mockPaymentAttemptDispatcherOld{},
 		Control:        makeMockControlTower(),