Commit 4e67896a authored by David Vorick's avatar David Vorick Committed by GitHub

Merge pull request #1570 from NebulousLabs/renewed-ids

Don't break downloads when renewing
parents 084ec26d 17c27e1e
......@@ -8,12 +8,14 @@ import (
"io/ioutil"
"net/url"
"path/filepath"
"strconv"
"strings"
"sync"
"testing"
"time"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
)
// TestHostAndRentVanilla sets up an integration test where a host and renter
......@@ -799,3 +801,213 @@ func TestRenterParallelDelete(t *testing.T) {
t.Fatal("file was not deleted properly:", rf.Files)
}
}
// TestRenterRenew sets up an integration test where a renter renews a
// contract with a host.
func TestRenterRenew(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
t.Parallel()
st, err := createServerTester("TestRenterRenew")
if err != nil {
t.Fatal(err)
}
defer st.server.Close()
// Announce the host and start accepting contracts.
err = st.announceHost()
if err != nil {
t.Fatal(err)
}
err = st.acceptContracts()
if err != nil {
t.Fatal(err)
}
err = st.setHostStorage()
if err != nil {
t.Fatal(err)
}
// Set an allowance for the renter, allowing a contract to be formed.
allowanceValues := url.Values{}
testFunds := "10000000000000000000000000000" // 10k SC
testPeriod := 10
allowanceValues.Set("funds", testFunds)
allowanceValues.Set("period", strconv.Itoa(testPeriod))
err = st.stdPostAPI("/renter", allowanceValues)
if err != nil {
t.Fatal(err)
}
// Create a file.
path := filepath.Join(st.dir, "test.dat")
err = createRandFile(path, 1024)
if err != nil {
t.Fatal(err)
}
// Upload the file to the renter.
uploadValues := url.Values{}
uploadValues.Set("source", path)
err = st.stdPostAPI("/renter/upload/test", uploadValues)
if err != nil {
t.Fatal(err)
}
// Only one piece will be uploaded (10% at current redundancy).
var rf RenterFiles
for i := 0; i < 200 && (len(rf.Files) != 1 || rf.Files[0].UploadProgress < 10); i++ {
st.getAPI("/renter/files", &rf)
time.Sleep(100 * time.Millisecond)
}
if len(rf.Files) != 1 || rf.Files[0].UploadProgress < 10 {
t.Fatal("the uploading is not succeeding for some reason:", rf.Files[0])
}
// Get current contract ID.
var rc RenterContracts
err = st.getAPI("/renter/contracts", &rc)
if err != nil {
t.Fatal(err)
}
contractID := rc.Contracts[0].ID
// Mine enough blocks to enter the renewal window.
for i := 0; i < testPeriod/2; i++ {
st.miner.AddBlock()
}
// Wait for the contract to be renewed.
for i := 0; i < 200 && (len(rc.Contracts) != 1 || rc.Contracts[0].ID == contractID); i++ {
st.getAPI("/renter/contracts", &rc)
time.Sleep(100 * time.Millisecond)
}
if rc.Contracts[0].ID == contractID {
t.Fatal("contract was not renewed:", rc.Contracts[0])
}
// Try downloading the file.
downpath := filepath.Join(st.dir, "testdown.dat")
err = st.stdGetAPI("/renter/download/test?destination=" + downpath)
if err != nil {
t.Fatal(err)
}
// Check that the download has the right contents.
orig, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
download, err := ioutil.ReadFile(downpath)
if err != nil {
t.Fatal(err)
}
if bytes.Compare(orig, download) != 0 {
t.Fatal("data mismatch when downloading a file")
}
}
// TestRenterAllowance sets up an integration test where a renter attempts to
// download a file after changing the allowance.
func TestRenterAllowance(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
t.Parallel()
st, err := createServerTester("TestRenterAllowance")
if err != nil {
t.Fatal(err)
}
defer st.server.Close()
// Announce the host and start accepting contracts.
err = st.announceHost()
if err != nil {
t.Fatal(err)
}
err = st.acceptContracts()
if err != nil {
t.Fatal(err)
}
err = st.setHostStorage()
if err != nil {
t.Fatal(err)
}
// Set an allowance for the renter, allowing a contract to be formed.
allowanceValues := url.Values{}
testFunds := types.SiacoinPrecision.Mul64(10000) // 10k SC
testPeriod := 20
allowanceValues.Set("funds", testFunds.String())
allowanceValues.Set("period", strconv.Itoa(testPeriod))
err = st.stdPostAPI("/renter", allowanceValues)
if err != nil {
t.Fatal(err)
}
// Create a file.
path := filepath.Join(st.dir, "test.dat")
err = createRandFile(path, 1024)
if err != nil {
t.Fatal(err)
}
// Upload the file to the renter.
uploadValues := url.Values{}
uploadValues.Set("source", path)
err = st.stdPostAPI("/renter/upload/test", uploadValues)
if err != nil {
t.Fatal(err)
}
// Only one piece will be uploaded (10% at current redundancy).
var rf RenterFiles
for i := 0; i < 200 && (len(rf.Files) != 1 || rf.Files[0].UploadProgress < 10); i++ {
st.getAPI("/renter/files", &rf)
time.Sleep(100 * time.Millisecond)
}
if len(rf.Files) != 1 || rf.Files[0].UploadProgress < 10 {
t.Fatal("the uploading is not succeeding for some reason:", rf.Files[0])
}
// Try downloading the file after modifying the allowance in various ways.
allowances := []struct {
funds types.Currency
period int
}{
{testFunds.Mul64(10), testPeriod / 2},
{testFunds, testPeriod / 2},
{testFunds.Div64(10), testPeriod / 2},
{testFunds.Mul64(10), testPeriod},
{testFunds, testPeriod},
{testFunds.Div64(10), testPeriod},
{testFunds.Mul64(10), testPeriod * 2},
{testFunds, testPeriod * 2},
{testFunds.Div64(10), testPeriod * 2},
}
for _, a := range allowances {
allowanceValues.Set("funds", a.funds.String())
allowanceValues.Set("period", strconv.Itoa(a.period))
err = st.stdPostAPI("/renter", allowanceValues)
if err != nil {
t.Fatal(err)
}
// Try downloading the file.
downpath := filepath.Join(st.dir, "testdown.dat")
err = st.stdGetAPI("/renter/download/test?destination=" + downpath)
if err != nil {
t.Fatal(err)
}
// Check that the download has the right contents.
orig, err := ioutil.ReadFile(path)
if err != nil {
t.Fatal(err)
}
download, err := ioutil.ReadFile(downpath)
if err != nil {
t.Fatal(err)
}
if bytes.Compare(orig, download) != 0 {
t.Fatal("data mismatch when downloading a file")
}
}
}
......@@ -83,6 +83,8 @@ func (c *Contractor) SetAllowance(a modules.Allowance) error {
return ErrInsufficientAllowance
}
c.log.Println("INFO: setting allowance to", a)
c.mu.RLock()
shouldRenew := a.Period != c.allowance.Period || !a.Funds.Equals(c.allowance.Funds)
shouldWait := c.blockHeight+a.Period < c.contractEndHeight()
......@@ -122,14 +124,16 @@ func (c *Contractor) SetAllowance(a modules.Allowance) error {
// renew existing contracts with new allowance parameters
newContracts := make(map[types.FileContractID]modules.RenterContract)
renewedIDs := make(map[types.FileContractID]types.FileContractID)
for _, contract := range renewSet {
newContract, err := c.managedRenew(contract, numSectors, endHeight)
if err != nil {
c.log.Printf("WARN: failed to renew contract with %v; a new contract will be formed in its place", contract.NetAddress)
c.log.Printf("WARN: failed to renew contract with %v (error: %v); a new contract will be formed in its place", contract.NetAddress, err)
remaining++
continue
}
newContracts[newContract.ID] = newContract
renewedIDs[contract.ID] = newContract.ID
if len(newContracts) >= int(a.Hosts) {
break
}
......@@ -164,6 +168,10 @@ func (c *Contractor) SetAllowance(a modules.Allowance) error {
}
// replace the current contract set with new contracts
c.contracts = newContracts
// link the contracts that were renewed
for oldID, newID := range renewedIDs {
c.renewedIDs[oldID] = newID
}
// if the currentPeriod was previously unset, set it now
if c.currentPeriod == 0 {
c.currentPeriod = periodStart
......@@ -211,6 +219,7 @@ func (c *Contractor) managedFormAllowanceContracts(n int, numSectors uint64, a m
// managedCancelAllowance handles the special case where the allowance is empty.
func (c *Contractor) managedCancelAllowance(a modules.Allowance) error {
c.log.Println("INFO: canceling allowance")
// first need to invalidate any active editors/downloaders
// NOTE: this code is the same as in managedRenewContracts
var ids []types.FileContractID
......
......@@ -117,10 +117,10 @@ func (c *Contractor) CurrentPeriod() types.BlockHeight {
return c.currentPeriod
}
// resolveID returns the ID of the most recent renewal of id.
func (c *Contractor) resolveID(id types.FileContractID) types.FileContractID {
// ResolveID returns the ID of the most recent renewal of id.
func (c *Contractor) ResolveID(id types.FileContractID) types.FileContractID {
if newID, ok := c.renewedIDs[id]; ok && newID != id {
return c.resolveID(newID)
return c.ResolveID(newID)
}
return id
}
......
......@@ -141,7 +141,7 @@ func TestContracts(t *testing.T) {
}
}
// TestResolveID tests the resolveID method.
// TestResolveID tests the ResolveID method.
func TestResolveID(t *testing.T) {
c := &Contractor{
renewedIDs: map[types.FileContractID]types.FileContractID{
......@@ -163,7 +163,7 @@ func TestResolveID(t *testing.T) {
{types.FileContractID{5}, types.FileContractID{6}},
}
for _, test := range tests {
if r := c.resolveID(test.id); r != test.resolved {
if r := c.ResolveID(test.id); r != test.resolved {
t.Errorf("expected %v -> %v, got %v", test.id, test.resolved, r)
}
}
......
......@@ -107,7 +107,7 @@ func (hd *hostDownloader) Close() error {
// from a host.
func (c *Contractor) Downloader(id types.FileContractID) (_ Downloader, err error) {
c.mu.RLock()
id = c.resolveID(id)
id = c.ResolveID(id)
cachedDownloader, haveDownloader := c.downloaders[id]
height := c.blockHeight
contract, haveContract := c.contracts[id]
......
......@@ -166,7 +166,7 @@ func (he *hostEditor) Modify(oldRoot, newRoot crypto.Hash, offset uint64, newDat
// delete sectors on a host.
func (c *Contractor) Editor(id types.FileContractID) (_ Editor, err error) {
c.mu.RLock()
id = c.resolveID(id)
id = c.ResolveID(id)
cachedEditor, haveEditor := c.editors[id]
height := c.blockHeight
contract, haveContract := c.contracts[id]
......
......@@ -117,7 +117,7 @@ type (
)
// newDownload initializes and returns a download object.
func newDownload(f *file, destination string) *download {
func (r *Renter) newDownload(f *file, destination string, currentContracts map[modules.NetAddress]types.FileContractID) *download {
d := &download{
finishedChunks: make([]bool, f.numChunks()),
......@@ -147,8 +147,17 @@ func newDownload(f *file, destination string) *download {
}
f.mu.RLock()
for _, contract := range f.contracts {
// get latest contract ID
id, ok := currentContracts[contract.IP]
if !ok {
// no matching NetAddress; try using a revised ID
id = r.hostContractor.ResolveID(contract.ID)
if id == contract.ID {
continue
}
}
for i := range contract.Pieces {
d.pieceSet[contract.Pieces[i].Chunk][contract.ID] = contract.Pieces[i]
d.pieceSet[contract.Pieces[i].Chunk][id] = contract.Pieces[i]
}
}
f.mu.RUnlock()
......
......@@ -5,6 +5,7 @@ import (
"sync/atomic"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/Sia/types"
)
// Download downloads a file, identified by its path, to the destination
......@@ -18,8 +19,14 @@ func (r *Renter) Download(path, destination string) error {
return errors.New("no file with that path")
}
// Build current contracts map.
currentContracts := make(map[modules.NetAddress]types.FileContractID)
for _, contract := range r.hostContractor.Contracts() {
currentContracts[contract.NetAddress] = contract.ID
}
// Create the download object and add it to the queue.
d := newDownload(file, destination)
d := r.newDownload(file, destination, currentContracts)
lockID = r.mu.Lock()
r.downloadQueue = append(r.downloadQueue, d)
r.mu.Unlock(lockID)
......
......@@ -100,6 +100,9 @@ type hostContractor interface {
// Downloader creates a Downloader from the specified contract ID,
// allowing the retrieval of sectors.
Downloader(types.FileContractID) (contractor.Downloader, error)
// ResolveID returns the most recent renewal of the specified ID.
ResolveID(types.FileContractID) types.FileContractID
}
// A trackedFile contains metadata about files being tracked by the Renter.
......@@ -201,6 +204,16 @@ func newRenter(cs modules.ConsensusSet, tpool modules.TransactionPool, hdb hostD
go r.threadedRepairLoop()
go r.threadedDownloadLoop()
go r.threadedQueueRepairs()
// Kill workers on shutdown.
r.tg.OnStop(func() {
id := r.mu.RLock()
for _, worker := range r.workerPool {
close(worker.killChan)
}
r.mu.RUnlock(id)
})
return r, nil
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment