Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions db/downloader/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Downloader Components

Main properties:

- initial download complete (`preverified.toml` on disk): the local `data-file` outranks the remote manifest and is never moved aside. A missing `data-file` is still downloaded. A local `.torrent` with a different infohash is dropped on this path (`addPreverifiedSnapshotForDownload` retains one that is already loaded and seeds under its hash), and only its sizes are used to check the file: on a mismatch the data backs neither manifest, so the download goes ahead under the preverified hash and the client completes the file by length, re-fetching only if the length disagrees. Otherwise the preverified download is skipped.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parenthetical scopes seeding under the local infohash to a torrent that is already loaded. The not-loaded case reaches the same outcome by another route, and the README doesn't say so.

keptLocal=true -> seedKeptSnapshot -> AddNewSeedableFile -> BuildTorrentIfNeed, which returns early because torrentFiles.Exists is a bare file-presence check with no infohash validation and finds the stale .torrent; then addCompleteTorrent -> loadMetainfoFromDisk -> addCompleteTorrentFromMetainfo -> addTorrentFromMetainfo(mi, name, mi.HashInfoBytes()) registers the snapshot under the stale local infohash. The name lands in torrentsByName, hence in allActiveSnapshots and PublishLocalChainToml, advertised under a hash that is not in the manifest.

Reachable state: preverified.toml present, accounts.0-1024.kv present with a stale accounts.0-1024.kv.torrent beside it whose sizes still match (so localMetainfoUnbacked is false and the file is kept), nothing loaded yet, download requested under the preverified hash.

An operator debugging why a seedbox advertises a hash absent from the manifest reads this line, concludes it's confined to already-loaded torrents, and looks in the wrong place.

"dropped" earlier in the sentence is fine — the clause continues "and only its sizes are used to check the file", which makes clear only the infohash is disregarded.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, and this is fixed on main in #23459 (this backport is a faithful cherry-pick of #23350, so it still carries the old wording).

The line there now reads: a local .torrent whose infohash differs from the preverified one "is ignored for the download decision but is left on disk, and only its sizes are used to check the file"; the sentence ends "the preverified download is skipped and the kept snapshot is seeded, under whatever infohash its local .torrent carries". No addPreverifiedSnapshotForDownload parenthetical, so nothing scopes it to an already-loaded torrent — which is the route you traced (BuildTorrentIfNeed returns early on bare file presence, then loadMetainfoFromDisk registers under the stale hash).

- initial download not complete: the remote manifest wins. Fetch the chain manifest (`chain.toml`) and align the datadir to it (remote `.torrent` and `data-file`s); data whose local `.torrent` infohash does not match the preverified one — including data with no readable `.torrent` at all — is renamed to `.part` and re-fetched. `preverified.toml` is written locally once the set is complete, never downloaded.

Deriving a `.torrent` from a bare `data-file` happens on the seeding path (`AddNewSeedableFile`), not on either path above.
Comment thread
AskAlexSharov marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Deriving a .torrent from a bare data-file happens on the seeding path (AddNewSeedableFile), not on either path above" — this same commit makes the complete path (the bullet at line 5) the trigger for that derivation.

prepareLocalDataForDownload returns download=false for a kept file, addTorrentForPreverifiedSnapshot sets keptLocal=true, download-batch.go:47 spawns seedKeptSnapshot, and that calls AddNewSeedableFile -> BuildTorrentIfNeed, which builds the metainfo when none is on disk. TestKeptLocalSnapshotIsSeeded pins exactly this: it writes only the data file, no .torrent, and asserts the name reaches torrentsByName — reachable only through a derived metainfo.

An operator on a restored datadir (preverified.toml + data, no .torrent files) sees the downloader hash every snapshot during a Download call, reads this line, and rules out the download path. It is the only cause.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same — fixed on main in #23459. Line 8 there now says derivation "happens on the seeding path (AddNewSeedableFile), which the complete path reaches: a kept snapshot is seeded, so metainfo that is missing, or present but malformed, is rebuilt from the data. The incomplete path never derives one — it fetches the remote .torrent instead."

So the operator on a restored datadir is pointed at the complete path, not away from it.


The diagram below shows the components used to manage downloads between torrents and WebSeeds.

![components](components.png)
Expand Down
11 changes: 9 additions & 2 deletions db/downloader/download-batch.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,24 @@ func (me *downloadBatch) taskWaiter() {
}

func (me *downloadBatch) addDownload(item preverifiedSnapshot) error {
t, first, miOpt, err := me.d.addPreverifiedSnapshotForDownload(item.InfoHash, item.Name)
snapshotTorrent, first, localMetainfo, keptLocal, err := me.d.addPreverifiedSnapshotForDownload(item.InfoHash, item.Name)
if err != nil {
return err
}
if keptLocal {
me.all.Go(func() { me.d.seedKeptSnapshot(me.d.ctx, item.Name) })
Comment thread
AskAlexSharov marked this conversation as resolved.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking on a release branch. One unbounded, uncancellable whole-file-hashing goroutine per kept item.

me.all is a plain sync.WaitGroup alias, so .Go has no bound. The task captures me.d.ctx, which is context.WithCancel(context.Background()) from downloader.go:331 and is not derived from the caller — so abandon()'s me.cancel(...) (line 92) never reaches it, and abandon() then blocks in me.all.Wait().

What it waits on: seedKeptSnapshot -> AddNewSeedableFile -> BuildTorrentIfNeed -> info.BuildFromFilePath -> GeneratePieces, which opens and SHA-1s the entire data file and takes no context at all. BuildTorrentIfNeed checks ctx.Done() once on entry (util.go:158) and never again. Each in-flight hash holds an open fd plus a 2 MiB DefaultPieceSize buffer.

Trigger: a datadir with preverified.toml and snapshot data but no .torrent files — rsync'd or restored. Nothing is in torrentsByName, so every item resolves to data-present / not-unbacked / download=false, keptLocal=true. The request carries the whole filtered preverified set (6,687 entries on the mainnet fixture), so that is thousands of concurrent goroutines each streaming a whole snapshot file: fd exhaustion and heavy disk contention while hundreds of GB hash at once. On interrupt, wait(ctx) returns but defer me.abandon() hangs until every remaining file finishes hashing — and in the seedbox command d.Close() cannot cancel d.ctx until that returns. Individual seed failures only log at warn (downloader.go:1199-1201) while the Download RPC still reports success.

My reviewers split on whether erigon nodes reach this or only cmd/downloader --seedbox (SyncSnapshots skips the request when snapCfg.Local is set, snapshotsync.go:411). I did not settle it — but the restored-datadir seedbox case is the one this change exists to serve, so it is in scope either way.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on the unbounded/uncancellable hashing — that is the same defect #23472 is written against, so it gets fixed on main.

One correction on what that fix currently buys, from reviewing #23472 at its current head: the bound lands, but the uninterruptible wait does not go away. wait() samples context.Cause(ctx) once and then abandon blocks in all.Wait(), so a cancel arriving during the drain is never observed — and for exactly the trigger you describe (preverified.toml + data, no .torrent) every item is kept-local, so me.torrents is empty, the loop returns nil immediately, and abandon(nil) waits for every file to finish hashing. Tasks already past the semaphore also still run under d.ctx, so up to seedConcurrency whole-file hashes remain uncancellable.

So your "on interrupt, d.Close() cannot cancel d.ctx until that returns" still holds after the bound. That needs settling on main before any of this reaches a release branch.

}
if !snapshotTorrent.Ok {
return nil
}
t := snapshotTorrent.Value
me.torrents = append(me.torrents, t)
if !first {
return nil
}
me.metainfoTasks.Go(func() {
me.doMetainfoTask(func() func() {
return me.d.addedFirstDownloader(me.d.ctx, t, miOpt, item.Name, item.InfoHash)
return me.d.addedFirstDownloader(me.d.ctx, t, localMetainfo, item.Name, item.InfoHash)
})
})
return nil
Expand Down
212 changes: 157 additions & 55 deletions db/downloader/downloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import (

"github.com/erigontech/erigon/common"
"github.com/erigontech/erigon/common/dbg"
"github.com/erigontech/erigon/common/dir"
"github.com/erigontech/erigon/common/log/v3"
"github.com/erigontech/erigon/db/datadir"
"github.com/erigontech/erigon/db/downloader/downloadercfg"
Expand Down Expand Up @@ -493,8 +494,18 @@ func (d *Downloader) StartTorrentPeerManager(ctx context.Context) {
})
}

// Check snapshot data looks right.
func (d *Downloader) snapshotDataLooksComplete(info *metainfo.Info) bool {
// preverified.toml is written once the initial snapshot set completes, pinning the local hash set.
// Read on every call: the snapshot stage writes the file mid-run, and from that moment local data
// must be kept.
func (d *Downloader) initialDownloadComplete() (bool, error) {
complete, err := dir.FileExist(d.cfg.Dirs.PreverifiedPath())
if err != nil {
return false, fmt.Errorf("checking %v: %w", d.cfg.Dirs.PreverifiedPath(), err)
}
return complete, nil
}

func (d *Downloader) snapshotDataSizesMatch(info *metainfo.Info) bool {
for f := range info.UpvertedFilesIter() {
pathParts := append([]string{info.BestName()}, f.BestPath()...)
slashPath := path.Join(pathParts...)
Expand Down Expand Up @@ -774,7 +785,7 @@ func (d *Downloader) loadMetainfoFromDisk(name string) (mi *metainfo.MetaInfo, e

// Loads metainfo from disk, removing it if it's invalid. Returns Some metainfo if it's valid. Logs
// errors.
func (d *Downloader) maybeLoadMetainfoFromDisk(name string) (miOpt g.Option[*metainfo.MetaInfo], err error) {
func (d *Downloader) maybeLoadMetainfoFromDisk(name string) (localMetainfo g.Option[*metainfo.MetaInfo], err error) {
miPath := d.metainfoFilePathForName(name)
mi, err := metainfo.LoadFromFile(miPath)
if err != nil {
Expand All @@ -783,7 +794,7 @@ func (d *Downloader) maybeLoadMetainfoFromDisk(name string) (miOpt g.Option[*met
}
return
}
miOpt.Set(mi)
localMetainfo.Set(mi)
return
}

Expand Down Expand Up @@ -1000,39 +1011,58 @@ func (d *Downloader) testStartSingleDownloadNoWait(
return err
}

func (d *Downloader) invalidateData(name snapshotName, infoHash metainfo.Hash) (err error) {
_, ok := d.torrentClient.Torrent(infoHash)
// Moves data aside so a download can't reuse it. Only legal while the initial download is
// incomplete: after that the local files are what this node built or restored, and nothing may
// remove them.
func (d *Downloader) invalidateData(name snapshotName, preverifiedInfoHash metainfo.Hash) (err error) {
complete, err := d.initialDownloadComplete()
if err != nil {
return err
}
if complete {
return fmt.Errorf("refusing to invalidate %q: initial download is complete", name)
}
_, ok := d.torrentClient.Torrent(preverifiedInfoHash)
// Torrent in use, bad idea to proceed. This shouldn't happen since we should have found
// the existing name earlier.
panicif.True(ok)
// Ensure the data isn't reused. We're presuming the storage in use, but we can't afford
// to wait until another torrent is fetched, and then we mistake a non-partial file with
// the correct size as being complete.
err = os.Rename(d.filePathForName(name), d.filePathForName(name+".part"))
if err != nil && errors.Is(err, os.ErrNotExist) {
err = nil
from := d.filePathForName(name)
to := d.filePathForName(name + ".part")
err = os.Rename(from, to)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
err = nil
}
return
}
d.log(log.LvlWarn, "invalidated local snapshot data, will re-download",
"name", name, "renamed_to", to, "preverified", preverifiedInfoHash)
return
}

// Download a preverified file. That means it has a published manifest (metainfo), and a known info
// hash. Caller is responsible for flushing missing metainfos to disk when complete.
func (d *Downloader) addPreverifiedSnapshotForDownload(
infoHash metainfo.Hash,
preverifiedInfoHash metainfo.Hash,
name string,
) (
t *torrent.Torrent,
// First add of this Torrent that asked to download. The caller is responsible for adding
// download tasks.
firstDownloader bool,
miOpt g.Option[*metainfo.MetaInfo],
// None when local data wins.
snapshotTorrent g.Option[*torrent.Torrent],
firstDownloader bool, // First add of this Torrent that asked to download. The caller is responsible for adding download tasks.
localMetainfo g.Option[*metainfo.MetaInfo],
// Local data was kept and no torrent is registered for it. The caller seeds it, off d.lock:
// deriving a metainfo hashes the whole file.
keptLocal bool,
err error,
) {
// Prevent anyone else from trying to add a torrent in the meanwhile, so we can do data
// invalidation, and identify the first downloader.
d.lock.Lock()
defer d.lock.Unlock()
t, ok, err := d.getExistingSnapshotTorrent(name, infoHash)
t, ok, err := d.getExistingSnapshotTorrent(name, preverifiedInfoHash)
if err != nil {
// If a torrent for this name is already loaded with a different infohash, keep the
// existing local torrent and skip the preverified download. This handles the case where
Expand All @@ -1043,87 +1073,159 @@ func (d *Downloader) addPreverifiedSnapshotForDownload(
// by AddTorrentsFromDisk before initial sync runs) is tracked separately. The proper fix
// is to run initial sync before AddTorrentsFromDisk so the preverified TOML hashes can
// always take precedence. See: https://github.com/erigontech/erigon/issues/19435
if existingT, nameOk := d.torrentsByName[name]; nameOk && existingT.InfoHash() != infoHash {
if existingT, nameOk := d.torrentsByName[name]; nameOk && existingT.InfoHash() != preverifiedInfoHash {
d.log(log.LvlWarn, "snapshot already loaded with different infohash, keeping existing local torrent (preverified skipped)",
"name", name,
"existing_infohash", existingT.InfoHash().HexString(),
"preverified_infohash", infoHash.HexString())
t = existingT
"preverified_infohash", preverifiedInfoHash.HexString())
snapshotTorrent.Set(existingT)
err = nil
return
}
return
}
// We can invalidate data if a torrent isn't yet loaded.
if !ok {
miOpt, err = d.loadMatchingMetainfoOrInvalidateData(infoHash, name)
if err != nil {
return
}
var new bool
t, new, err = d.addTorrent(name, infoHash)
if err != nil {
var isNew bool
var addedTorrent g.Option[*torrent.Torrent]
addedTorrent, isNew, localMetainfo, keptLocal, err = d.addTorrentForPreverifiedSnapshot(preverifiedInfoHash, name)
if err != nil || !addedTorrent.Ok {
return
}
panicif.False(new)
panicif.False(isNew)
t = addedTorrent.Value
}
g.MakeMapIfNil(&d.downloads)
firstDownloader = !g.MapInsert(d.downloads, t, struct{}{}).Ok
snapshotTorrent.Set(t)
return
}

func (d *Downloader) loadMatchingMetainfoOrInvalidateData(
infoHash metainfo.Hash,
func (d *Downloader) addTorrentForPreverifiedSnapshot(
preverifiedInfoHash metainfo.Hash,
name string,
) (
miOpt g.Option[*metainfo.MetaInfo],
// None when local data wins.
addedTorrent g.Option[*torrent.Torrent],
isNew bool,
localMetainfo g.Option[*metainfo.MetaInfo],
keptLocal bool,
err error,
) {
miOpt, err = d.maybeLoadMetainfoFromDisk(name)
var download bool
localMetainfo, download, err = d.prepareLocalDataForDownload(preverifiedInfoHash, name)
if err != nil || !download {
keptLocal = err == nil
return
}
t, isNew, err := d.addTorrent(name, preverifiedInfoHash)
if err != nil {
d.log(log.LvlError, "error loading metainfo from disk", "err", err, "name", name)
err = nil
return
}
if miOpt.Ok {
loadedIh := miOpt.Value.HashInfoBytes()
if loadedIh == infoHash {
return
addedTorrent.Set(t)
return
}

// Reports whether the preverified download should go ahead. Data that backs no matching metainfo is
// invalidated, but only while the manifest is still authoritative.
func (d *Downloader) prepareLocalDataForDownload(
preverifiedInfoHash metainfo.Hash,
name string,
) (
localMetainfo g.Option[*metainfo.MetaInfo],
download bool,
err error,
) {
// An unreadable metainfo is logged and then treated as missing: it says nothing about the data.
localMetainfo, loadErr := d.maybeLoadMetainfoFromDisk(name)
if loadErr != nil {
d.log(log.LvlWarn, "error loading metainfo from disk", "err", loadErr, "name", name)
}
localMetainfoUnbacked := false
if localMetainfo.Ok {
localInfoHash := localMetainfo.Value.HashInfoBytes()
if localInfoHash == preverifiedInfoHash {
return localMetainfo, true, nil
}
// This is fine if we're doing initial sync. If we're not we shouldn't be here.
d.log(log.LvlWarn, "preverified snapshot hash has changed",
"expected", infoHash,
"actual", loadedIh,
"preverified", preverifiedInfoHash,
"local", localInfoHash,
"name", name)
info, infoErr := localMetainfo.Value.UnmarshalInfo()
if infoErr != nil {
d.log(log.LvlWarn, "error unmarshalling local metainfo", "err", infoErr, "name", name)
}
localMetainfoUnbacked = infoErr == nil && !d.snapshotDataSizesMatch(&info)
// Forget the metainfo we loaded, it's wrong (probably changed hash but not name...)
miOpt.SetNone()
localMetainfo.SetNone()
} else {
d.log(log.LvlDebug, "snapshot metainfo missing", "name", name)
}
err = d.invalidateData(name, infoHash)

complete, err := d.initialDownloadComplete()
if err != nil {
err = fmt.Errorf("invalidating old snapshot data: %w", err)
return
return localMetainfo, false, err
}
return
if complete {
// Local data outranks the manifest from here on: keep whatever we have, download the rest.
exists, err := d.snapshotDataExists(name)
if err != nil {
return localMetainfo, false, err
}
if !exists {
return localMetainfo, true, nil
}
if localMetainfoUnbacked {
// The data backs neither manifest. Downloading hands it to the client, which
// completes the file by length, so a wrong length is re-fetched while
// same-length-different-bytes is not.
d.log(log.LvlWarn, "local snapshot does not match its own metainfo, downloading",
"name", name)
return localMetainfo, true, nil
}
d.log(log.LvlWarn, "keeping local snapshot, skipping preverified download", "name", name)
return localMetainfo, false, nil
}

if err := d.invalidateData(name, preverifiedInfoHash); err != nil {
return localMetainfo, false, fmt.Errorf("invalidating old snapshot data: %w", err)
}
return localMetainfo, true, nil
}

// seedKeptSnapshot registers a kept local snapshot so it is seeded, deriving the metainfo when

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cost isn't the binding constraint here, and the same wording is at downloader.go:1055-1056.

seedKeptSnapshot -> AddNewSeedableFile -> addCompleteTorrent takes d.lock.Lock() itself at downloader.go:1411, and d.lock is a sync.RWMutex — not reentrant. Calling this under the lock doesn't stall on hashing, it deadlocks the downloader with every d.lock reader stuck behind it. The correct reason is already stated for the analogous case at downloader.go:399-400 ("We hold the downloader lock but spawn also takes it").

The failure a cost-based comment permits: someone decides a goroutine is overkill for a small .kv and inlines d.AddNewSeedableFile(ctx, name) into addPreverifiedSnapshotForDownload, which holds d.lock for its whole body. "This file is 2 MB, hashing is instant" passes review, and the process hangs on the first kept snapshot.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and fixed on main in #23459. The seedKeptSnapshot docstring there leads with the re-entrancy: "Must run without d.lock: addCompleteTorrent takes it and the lock is not reentrant, so calling this under it deadlocks. Deriving also hashes the whole file." The copy at addPreverifiedSnapshotForDownload is a pointer to it rather than a second statement of the same rationale.

// none is on disk. Must run without d.lock: deriving it hashes the whole file.
Comment thread
AskAlexSharov marked this conversation as resolved.
func (d *Downloader) seedKeptSnapshot(ctx context.Context, name string) {
if err := d.AddNewSeedableFile(ctx, name); err != nil && ctx.Err() == nil {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept torrents get registered without trackers, so nothing announces them.

AddNewSeedableFile calls addCompleteTorrent and discards the torrent it returns — _, _, err = d.addCompleteTorrent(name) — so afterAdd never runs, and afterAdd (downloader.go:1882-1884) is where t.AddTrackers(Trackers) happens. addTorrentFromMetainfo only applies info bytes and piece layers. The freshly generated .torrent does carry the tracker list on disk, but the live torrent never consumes that outer metainfo.

Reachable in cmd/downloader --seedbox: AddTorrentsFromDisk runs before the changed Download path, so a restored datadir with no .torrent files has nothing loaded, and that command does not run the node's TorrentPeerManager. Net effect is a seedbox that believes it is seeding and that no peer can discover.

The omission inside AddNewSeedableFile is pre-existing; what is new is that this change makes it the sole seeding route for every kept file without metainfo.

Single-source (conf 93) — I confirmed the afterAdd/AddTrackers mechanics above, but not the end-to-end "peers cannot discover it" behaviour. Worth a check before acting.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked this, since you flagged it as single-source. Confirmed, and it is worse than "no trackers".

makeAddTorrentOpts (downloader.go:1812-1818) sets ts.DisallowDataUpload = true on every torrent it adds, and the only place that lifts it is afterAdd -> t.AllowDataUpload() (downloader.go:1885), which is also the sole t.AddTrackers(Trackers) call site in the file. AddNewSeedableFile -> addCompleteTorrent -> addCompleteTorrentFromMetainfo -> addTorrentFromMetainfo never reaches afterAdd, so a kept snapshot is registered with data upload disallowed.

So it is not only that no peer can discover it — even a peer that finds it via DHT/PEX or a manual add gets nothing, because the torrent will not upload. The seeding is a no-op, and the Download RPC still reports success.

Agreed the omission inside AddNewSeedableFile predates #23350; what #23350 changes is that this becomes the only seeding route for kept files without metainfo. Fix belongs on main, not on this cherry-pick.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question rather than a defect claim — this is present identically on main in #23350, so it is not a backport artifact, but I could not find it adjudicated anywhere and the caller contract is release-branch-facing.

When a completed datadir has local snapshot data but no .torrent, addDownload schedules the seeding function and adds no torrent to the batch. If AddNewSeedableFile fails — the filesystem filling while it writes the metainfo, say — the error is only logged. The batch waits for the task but cannot observe its result, has no torrent to wait on, and returns success through the Download RPC. cmd/downloader --seedbox then keeps running with the kept file never registered and unservable.

Is the success return intended here, or should a seeding failure on a kept file fail the batch?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intended for the node, and your seedbox worry is real but a failed batch is the wrong lever.

Download promises the requested data is present locally, and on the kept path it already is — complete, byte-for-byte, the file the caller asked for. Failing the batch because the seeding side stumbled would make a node error out on data it fully has, and the retry re-runs exactly the same failing step. The seeding is a side effect of arriving at the kept state, not part of what the caller waited for.

The seedbox gap is real, and I found it is worse than the metainfo-write case you describe. Fixed on main in #23459 (3b4f608): AddNewSeedableFile stopped at addCompleteTorrent and never reached afterAdd, which is the only place that calls t.AddTrackers(Trackers) and t.AllowDataUpload(). Since makeAddTorrentOpts sets DisallowDataUpload = true on everything it adds, a kept snapshot was registered, counted in torrentsByName / allActiveSnapshots / PublishLocalChainToml — and uploaded nothing to anybody, tracker or DHT. So the success return was reporting a seedbox that seeded zero bytes even when AddNewSeedableFile returned no error at all; failing on its error would not have caught that.

TestKeptLocalSnapshotIsAnnouncedAndUploadable pins it (red before: AnnounceList empty).

If a seedbox should refuse to come up with unservable files, I would rather it assert that at the seedbox level — every preverified name resolves to a torrent that announces — than couple it to the download RPC. Happy to do that as a separate change if you want it.

d.log(log.LvlWarn, "cannot seed kept local snapshot", "err", err, "name", name)
}
Comment thread
AskAlexSharov marked this conversation as resolved.
}

func (d *Downloader) snapshotDataExists(name string) (bool, error) {
exists, err := dir.FileExist(d.filePathForName(name))
if err != nil {
return false, fmt.Errorf("checking snapshot data for %q: %w", name, err)
}
return exists, nil
}

func (d *Downloader) addedFirstDownloader(
ctx context.Context,
t *torrent.Torrent,
miOpt g.Option[*metainfo.MetaInfo],
localMetainfo g.Option[*metainfo.MetaInfo],
name string,
infoHash metainfo.Hash,
) (afterAdd func()) {
// Try again, we would have invalidated data for changed infohashes now.
if !miOpt.Ok {
// Try the webseeds for the metainfo that wasn't on disk. Nothing here relies on the data having
// been moved aside: after the initial download it never is, and the client completes the file
// by length.
if !localMetainfo.Ok {
// Yes I mean for this error to be scoped here.
err := d.fetchMetainfoFromWebseeds(ctx, name, infoHash)
if err == nil {
// Always reuse code paths to ensure no surprises later. I.e. load the metainfo again
// through the same path that is used on a good run. No data invalidation here, at this
// point we've added the torrent, and already invalidated if the metainfo was missing
// the first time.
miOpt, err = d.maybeLoadMetainfoFromDisk(name)
// through the same path that is used on a good run.
localMetainfo, err = d.maybeLoadMetainfoFromDisk(name)
if err != nil {
// Should this error be returned instead?
d.log(log.LvlError, "error loading metainfo from disk", "err", err, "name", name)
Expand All @@ -1133,10 +1235,10 @@ func (d *Downloader) addedFirstDownloader(
}
}

if miOpt.Ok {
if localMetainfo.Ok {
// Good case: We have a metainfo with the right infohash, either just fetched from a
// webseed, or it was cached on disk.
err := d.applyMetainfo(miOpt.Value, t)
err := d.applyMetainfo(localMetainfo.Value, t)
if err != nil {
d.log(log.LvlError, "error applying metainfo", "err", err, "name", name)
}
Expand Down Expand Up @@ -1285,7 +1387,7 @@ func (d *Downloader) addTorrentIfComplete(
err = fmt.Errorf("unmarshalling info from metainfo: %w", err)
return
}
if !d.snapshotDataLooksComplete(&info) {
if !d.snapshotDataSizesMatch(&info) {
err = nil
return
}
Expand Down
Loading
Loading