2017-10-17 04:57:30 +03:00
|
|
|
package routing
|
|
|
|
|
|
|
|
import (
|
|
|
|
"sync"
|
|
|
|
"time"
|
2017-10-18 05:41:46 +03:00
|
|
|
|
2018-07-31 10:17:17 +03:00
|
|
|
"github.com/btcsuite/btcd/btcec"
|
2018-05-08 07:04:31 +03:00
|
|
|
"github.com/coreos/bbolt"
|
2017-10-18 05:41:46 +03:00
|
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
2018-03-27 06:53:46 +03:00
|
|
|
"github.com/lightningnetwork/lnd/lnwire"
|
2017-10-17 04:57:30 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
// vertexDecay is the decay period of colored vertexes added to
|
|
|
|
// missionControl. Once vertexDecay passes after an entry has been
|
|
|
|
// added to the prune view, it is garbage collected. This value is
|
|
|
|
// larger than edgeDecay as an edge failure typical indicates an
|
|
|
|
// unbalanced channel, while a vertex failure indicates a node is not
|
|
|
|
// online and active.
|
|
|
|
vertexDecay = time.Duration(time.Minute * 5)
|
|
|
|
|
|
|
|
// edgeDecay is the decay period of colored edges added to
|
|
|
|
// missionControl. Once edgeDecay passed after an entry has been added,
|
|
|
|
// it is garbage collected. This value is smaller than vertexDecay as
|
|
|
|
// an edge related failure during payment sending typically indicates
|
|
|
|
// that a channel was unbalanced, a condition which may quickly change.
|
|
|
|
//
|
|
|
|
// TODO(roasbeef): instead use random delay on each?
|
|
|
|
edgeDecay = time.Duration(time.Second * 5)
|
|
|
|
)
|
|
|
|
|
|
|
|
// missionControl contains state which summarizes the past attempts of HTLC
|
|
|
|
// routing by external callers when sending payments throughout the network.
|
|
|
|
// missionControl remembers the outcome of these past routing attempts (success
|
|
|
|
// and failure), and is able to provide hints/guidance to future HTLC routing
|
|
|
|
// attempts. missionControl maintains a decaying network view of the
|
|
|
|
// edges/vertexes that should be marked as "pruned" during path finding. This
|
|
|
|
// graph view acts as a shared memory during HTLC payment routing attempts.
|
|
|
|
// With each execution, if an error is encountered, based on the type of error
|
|
|
|
// and the location of the error within the route, an edge or vertex is added
|
|
|
|
// to the view. Later sending attempts will then query the view for all the
|
|
|
|
// vertexes/edges that should be ignored. Items in the view decay after a set
|
|
|
|
// period of time, allowing the view to be dynamic w.r.t network changes.
|
|
|
|
type missionControl struct {
|
|
|
|
// failedEdges maps a short channel ID to be pruned, to the time that
|
|
|
|
// it was added to the prune view. Edges are added to this map if a
|
|
|
|
// caller reports to missionControl a failure localized to that edge
|
|
|
|
// when sending a payment.
|
2019-03-05 13:13:44 +03:00
|
|
|
failedEdges map[EdgeLocator]time.Time
|
2017-10-17 04:57:30 +03:00
|
|
|
|
|
|
|
// failedVertexes maps a node's public key that should be pruned, to
|
|
|
|
// the time that it was added to the prune view. Vertexes are added to
|
|
|
|
// this map if a caller reports to missionControl a failure localized
|
|
|
|
// to that particular vertex.
|
2017-09-08 04:25:43 +03:00
|
|
|
failedVertexes map[Vertex]time.Time
|
2017-10-17 04:57:30 +03:00
|
|
|
|
2017-10-18 05:41:46 +03:00
|
|
|
graph *channeldb.ChannelGraph
|
|
|
|
|
|
|
|
selfNode *channeldb.LightningNode
|
|
|
|
|
2018-05-08 07:04:31 +03:00
|
|
|
queryBandwidth func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi
|
|
|
|
|
2017-10-17 04:57:30 +03:00
|
|
|
sync.Mutex
|
|
|
|
|
|
|
|
// TODO(roasbeef): further counters, if vertex continually unavailable,
|
|
|
|
// add to another generation
|
|
|
|
|
|
|
|
// TODO(roasbeef): also add favorable metrics for nodes
|
|
|
|
}
|
|
|
|
|
|
|
|
// newMissionControl returns a new instance of missionControl.
|
|
|
|
//
|
|
|
|
// TODO(roasbeef): persist memory
|
2018-06-07 06:33:55 +03:00
|
|
|
func newMissionControl(g *channeldb.ChannelGraph, selfNode *channeldb.LightningNode,
|
2018-05-08 07:04:31 +03:00
|
|
|
qb func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) *missionControl {
|
2017-10-18 05:41:46 +03:00
|
|
|
|
2017-10-17 04:57:30 +03:00
|
|
|
return &missionControl{
|
2019-03-05 13:13:44 +03:00
|
|
|
failedEdges: make(map[EdgeLocator]time.Time),
|
2017-09-08 04:25:43 +03:00
|
|
|
failedVertexes: make(map[Vertex]time.Time),
|
2017-10-23 03:28:18 +03:00
|
|
|
selfNode: selfNode,
|
2018-05-08 07:04:31 +03:00
|
|
|
queryBandwidth: qb,
|
2017-10-23 03:28:18 +03:00
|
|
|
graph: g,
|
2017-10-17 04:57:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
// graphPruneView is a filter of sorts that path finding routines should
|
|
|
|
// consult during the execution. Any edges or vertexes within the view should
|
|
|
|
// be ignored during path finding. The contents of the view reflect the current
|
|
|
|
// state of the wider network from the PoV of mission control compiled via HTLC
|
|
|
|
// routing attempts in the past.
|
|
|
|
type graphPruneView struct {
|
2019-03-05 13:13:44 +03:00
|
|
|
edges map[EdgeLocator]struct{}
|
2017-10-18 05:41:46 +03:00
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
vertexes map[Vertex]struct{}
|
2017-10-18 05:41:46 +03:00
|
|
|
}
|
|
|
|
|
2017-10-17 04:57:30 +03:00
|
|
|
// GraphPruneView returns a new graphPruneView instance which is to be
|
|
|
|
// consulted during path finding. If a vertex/edge is found within the returned
|
|
|
|
// prune view, it is to be ignored as a goroutine has had issues routing
|
|
|
|
// through it successfully. Within this method the main view of the
|
2018-02-07 06:11:11 +03:00
|
|
|
// missionControl is garbage collected as entries are detected to be "stale".
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
func (m *missionControl) GraphPruneView() graphPruneView {
|
2017-10-17 04:57:30 +03:00
|
|
|
// First, we'll grab the current time, this value will be used to
|
|
|
|
// determine if an entry is stale or not.
|
|
|
|
now := time.Now()
|
|
|
|
|
|
|
|
m.Lock()
|
|
|
|
|
|
|
|
// For each of the vertexes that have been added to the prune view, if
|
|
|
|
// it is now "stale", then we'll ignore it and avoid adding it to the
|
|
|
|
// view we'll return.
|
2017-09-08 04:25:43 +03:00
|
|
|
vertexes := make(map[Vertex]struct{})
|
2017-10-17 04:57:30 +03:00
|
|
|
for vertex, pruneTime := range m.failedVertexes {
|
2017-10-17 06:31:26 +03:00
|
|
|
if now.Sub(pruneTime) >= vertexDecay {
|
2017-10-17 04:57:30 +03:00
|
|
|
log.Tracef("Pruning decayed failure report for vertex %v "+
|
|
|
|
"from Mission Control", vertex)
|
|
|
|
|
|
|
|
delete(m.failedVertexes, vertex)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
vertexes[vertex] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// We'll also do the same for edges, but use the edgeDecay this time
|
|
|
|
// rather than the decay for vertexes.
|
2019-03-05 13:13:44 +03:00
|
|
|
edges := make(map[EdgeLocator]struct{})
|
2017-10-17 04:57:30 +03:00
|
|
|
for edge, pruneTime := range m.failedEdges {
|
|
|
|
if now.Sub(pruneTime) >= edgeDecay {
|
|
|
|
log.Tracef("Pruning decayed failure report for edge %v "+
|
|
|
|
"from Mission Control", edge)
|
|
|
|
|
|
|
|
delete(m.failedEdges, edge)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
edges[edge] = struct{}{}
|
|
|
|
}
|
|
|
|
|
|
|
|
m.Unlock()
|
|
|
|
|
|
|
|
log.Debugf("Mission Control returning prune view of %v edges, %v "+
|
|
|
|
"vertexes", len(edges), len(vertexes))
|
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
return graphPruneView{
|
2017-10-17 04:57:30 +03:00
|
|
|
edges: edges,
|
|
|
|
vertexes: vertexes,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
// NewPaymentSession creates a new payment session backed by the latest prune
|
2018-03-27 06:53:46 +03:00
|
|
|
// view from Mission Control. An optional set of routing hints can be provided
|
|
|
|
// in order to populate additional edges to explore when finding a path to the
|
|
|
|
// payment's destination.
|
|
|
|
func (m *missionControl) NewPaymentSession(routeHints [][]HopHint,
|
2019-03-05 18:55:19 +03:00
|
|
|
target Vertex) (*paymentSession, error) {
|
2018-03-27 06:53:46 +03:00
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
viewSnapshot := m.GraphPruneView()
|
|
|
|
|
2018-03-27 06:53:46 +03:00
|
|
|
edges := make(map[Vertex][]*channeldb.ChannelEdgePolicy)
|
|
|
|
|
|
|
|
// Traverse through all of the available hop hints and include them in
|
|
|
|
// our edges map, indexed by the public key of the channel's starting
|
|
|
|
// node.
|
|
|
|
for _, routeHint := range routeHints {
|
|
|
|
// If multiple hop hints are provided within a single route
|
|
|
|
// hint, we'll assume they must be chained together and sorted
|
|
|
|
// in forward order in order to reach the target successfully.
|
|
|
|
for i, hopHint := range routeHint {
|
|
|
|
// In order to determine the end node of this hint,
|
|
|
|
// we'll need to look at the next hint's start node. If
|
|
|
|
// we've reached the end of the hints list, we can
|
|
|
|
// assume we've reached the destination.
|
|
|
|
endNode := &channeldb.LightningNode{}
|
|
|
|
if i != len(routeHint)-1 {
|
|
|
|
endNode.AddPubKey(routeHint[i+1].NodeID)
|
|
|
|
} else {
|
2019-03-05 18:55:19 +03:00
|
|
|
targetPubKey, err := btcec.ParsePubKey(
|
|
|
|
target[:], btcec.S256(),
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
endNode.AddPubKey(targetPubKey)
|
2018-03-27 06:53:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// Finally, create the channel edge from the hop hint
|
|
|
|
// and add it to list of edges corresponding to the node
|
|
|
|
// at the start of the channel.
|
|
|
|
edge := &channeldb.ChannelEdgePolicy{
|
|
|
|
Node: endNode,
|
|
|
|
ChannelID: hopHint.ChannelID,
|
|
|
|
FeeBaseMSat: lnwire.MilliSatoshi(
|
|
|
|
hopHint.FeeBaseMSat,
|
|
|
|
),
|
|
|
|
FeeProportionalMillionths: lnwire.MilliSatoshi(
|
|
|
|
hopHint.FeeProportionalMillionths,
|
|
|
|
),
|
|
|
|
TimeLockDelta: hopHint.CLTVExpiryDelta,
|
|
|
|
}
|
|
|
|
|
|
|
|
v := NewVertex(hopHint.NodeID)
|
|
|
|
edges[v] = append(edges[v], edge)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-08 07:04:31 +03:00
|
|
|
// 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 don't have enough
|
|
|
|
// bandwidth to carry the payment.
|
|
|
|
sourceNode, err := m.graph.SourceNode()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
bandwidthHints, err := generateBandwidthHints(
|
|
|
|
sourceNode, m.queryBandwidth,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
return &paymentSession{
|
2018-10-24 10:59:38 +03:00
|
|
|
pruneViewSnapshot: viewSnapshot,
|
|
|
|
additionalEdges: edges,
|
|
|
|
bandwidthHints: bandwidthHints,
|
2019-03-05 13:13:44 +03:00
|
|
|
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
|
2018-10-24 10:59:38 +03:00
|
|
|
mc: m,
|
2018-05-08 07:04:31 +03:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2018-06-07 06:33:55 +03:00
|
|
|
// NewPaymentSessionFromRoutes creates a new paymentSession instance that will
|
|
|
|
// skip all path finding, and will instead utilize a set of pre-built routes.
|
|
|
|
// This constructor allows callers to specify their own routes which can be
|
|
|
|
// used for things like channel rebalancing, and swaps.
|
|
|
|
func (m *missionControl) NewPaymentSessionFromRoutes(routes []*Route) *paymentSession {
|
|
|
|
return &paymentSession{
|
2018-10-24 10:59:38 +03:00
|
|
|
pruneViewSnapshot: m.GraphPruneView(),
|
|
|
|
haveRoutes: true,
|
|
|
|
preBuiltRoutes: routes,
|
2019-03-05 13:13:44 +03:00
|
|
|
errFailedPolicyChans: make(map[EdgeLocator]struct{}),
|
2018-10-24 10:59:38 +03:00
|
|
|
mc: m,
|
2018-06-07 06:33:55 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-05-08 07:04:31 +03:00
|
|
|
// generateBandwidthHints is a helper function that's utilized the main
|
|
|
|
// findPath function in order to obtain hints from the lower layer w.r.t to the
|
|
|
|
// available bandwidth of edges on the network. Currently, we'll only obtain
|
|
|
|
// bandwidth hints for the edges we directly have open ourselves. Obtaining
|
|
|
|
// these hints allows us to reduce the number of extraneous attempts as we can
|
|
|
|
// skip channels that are inactive, or just don't have enough bandwidth to
|
|
|
|
// carry the payment.
|
|
|
|
func generateBandwidthHints(sourceNode *channeldb.LightningNode,
|
|
|
|
queryBandwidth func(*channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi) (map[uint64]lnwire.MilliSatoshi, error) {
|
|
|
|
|
|
|
|
// First, we'll collect the set of outbound edges from the target
|
|
|
|
// source node.
|
|
|
|
var localChans []*channeldb.ChannelEdgeInfo
|
2018-11-30 07:04:21 +03:00
|
|
|
err := sourceNode.ForEachChannel(nil, func(tx *bbolt.Tx,
|
2018-05-08 07:04:31 +03:00
|
|
|
edgeInfo *channeldb.ChannelEdgeInfo,
|
|
|
|
_, _ *channeldb.ChannelEdgePolicy) error {
|
|
|
|
|
|
|
|
localChans = append(localChans, edgeInfo)
|
|
|
|
return nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now that we have all of our outbound edges, we'll populate the set
|
|
|
|
// of bandwidth hints, querying the lower switch layer for the most up
|
|
|
|
// to date values.
|
|
|
|
bandwidthHints := make(map[uint64]lnwire.MilliSatoshi)
|
|
|
|
for _, localChan := range localChans {
|
|
|
|
bandwidthHints[localChan.ChannelID] = queryBandwidth(localChan)
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
}
|
2018-05-08 07:04:31 +03:00
|
|
|
|
|
|
|
return bandwidthHints, nil
|
routing: add new paymentSession companion struct to missionControl
In this commit, we modify the pruning semantics of the missionControl
struct. Before this commit, on each payment attempt, we would fetch a
new graph pruned view each time. This served to instantly propagate any
detected failures to all outstanding payment attempts. However, this
meant that we could at times get stuck in a retry loop if sends take a
few second, then we may prune an edge, try another, then the original
edge is now unpruned.
To remedy this, we now introduce the concept of a paymentSession. The
session will start out as a snapshot of the latest graph prune view.
Any payment failures are now reported directly to the paymentSession
rather than missionControl. The rationale for this is that
edges/vertexes pruned as result of failures will never decay for a
local payment session, only for the global prune view. With this in
place, we ensure that our set of prune view only grows for a session.
Fixes #536.
2018-01-09 06:38:49 +03:00
|
|
|
}
|
|
|
|
|
2017-10-17 04:57:30 +03:00
|
|
|
// ResetHistory resets the history of missionControl returning it to a state as
|
|
|
|
// if no payment attempts have been made.
|
|
|
|
func (m *missionControl) ResetHistory() {
|
|
|
|
m.Lock()
|
2019-03-05 13:13:44 +03:00
|
|
|
m.failedEdges = make(map[EdgeLocator]time.Time)
|
2017-09-08 04:25:43 +03:00
|
|
|
m.failedVertexes = make(map[Vertex]time.Time)
|
2017-10-17 04:57:30 +03:00
|
|
|
m.Unlock()
|
|
|
|
}
|