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: 5 additions & 2 deletions common/fe-ste-models.go
Original file line number Diff line number Diff line change
Expand Up @@ -874,10 +874,13 @@ func (TransferStatus) Started() TransferStatus { return TransferStatus(1) }
// Transfer successfully completed
func (TransferStatus) Success() TransferStatus { return TransferStatus(2) }

// Folder was created, but properties have not been persisted yet. Equivalent to Started, but never intended to be set on anything BUT folders.
// FolderCreated folder was created, but properties have not been persisted yet. Equivalent to Started, but never intended to be set on anything BUT folders.
func (TransferStatus) FolderCreated() TransferStatus { return TransferStatus(3) }

func (TransferStatus) Restarted() TransferStatus { return TransferStatus(4) }
// FolderExisted folder already existed before we got to it. A valid state to continue from in overwrite scenarios.
func (TransferStatus) FolderExisted() TransferStatus { return TransferStatus(4) }

func (TransferStatus) Restarted() TransferStatus { return TransferStatus(5) }

// Transfer failed due to some error.
func (TransferStatus) Failed() TransferStatus { return TransferStatus(-1) }
Expand Down
9 changes: 9 additions & 0 deletions common/folderCreationTracker_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,12 @@ type FolderCreationTracker interface {
type Prompter interface {
ShouldOverwrite(objectPath string, objectType EntityType) bool
}

// FolderCreationErrorAlreadyExists is a signalling error that should be
// returned by doCreation on FolderCreationTracker.CreateFolder for supported folder-creators.
// This will inform the folder creation tracker to _not_ try to create the folder.
Comment on lines +18 to +19
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

The comment is misleading. The error is returned after doCreation has already been called and attempted to create the folder. It signals that the folder already existed, not that creation should be skipped. Consider rewording to: "FolderCreationErrorAlreadyExists is a signalling error that should be returned by doCreation when the folder already exists, allowing the tracker to mark it as FolderExisted rather than FolderCreated."

Suggested change
// returned by doCreation on FolderCreationTracker.CreateFolder for supported folder-creators.
// This will inform the folder creation tracker to _not_ try to create the folder.
// returned by doCreation when the folder already exists, allowing the tracker to mark it as
// FolderExisted rather than FolderCreated.

Copilot uses AI. Check for mistakes.
type FolderCreationErrorAlreadyExists struct{}

func (f FolderCreationErrorAlreadyExists) Error() string {
return "not a real error"
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ require (
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.48.1 // indirect
github.com/acomagu/trie/v2 v2.0.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20240905190251-b4127c9b8d78 // indirect
Expand Down Expand Up @@ -86,6 +87,7 @@ require (
go.opentelemetry.io/otel/sdk v1.29.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.29.0 // indirect
go.opentelemetry.io/otel/trace v1.29.0 // indirect
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 // indirect
golang.org/x/text v0.27.0 // indirect
golang.org/x/time v0.7.0 // indirect
google.golang.org/genproto v0.0.0-20241015192408-796eee8c2d53 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/JeffreyRichter/enum v0.0.0-20180725232043-2567042f9cda h1:NOo6+gM9NNPJ3W56nxOKb4164LEw094U0C8zYQM8mQU=
github.com/JeffreyRichter/enum v0.0.0-20180725232043-2567042f9cda/go.mod h1:2CaSFTh2ph9ymS6goiOKIBdfhwWUVsX4nQ5QjIYFHHs=
github.com/PuerkitoBio/goquery v1.7.1/go.mod h1:XY0pP4kfraEmmV1O7Uf6XyjoslwsneBbgeDjLYuN8xY=
github.com/acomagu/trie/v2 v2.0.0 h1:4/Vt77FUj6qtYl7IN/2QMyl22ztBT0Cr3wg3kL/9mHg=
github.com/acomagu/trie/v2 v2.0.0/go.mod h1:trIf+o9oABbDJULhZ+jUiE5HjfO29H30dQV5PV+P8DA=
github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxBp0T0eFw1RUQY=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g=
Expand Down Expand Up @@ -212,6 +214,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 h1:LGJsf5LRplCck6jUCH3dBL2dmycNruWNF5xugkSlfXw=
golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
Expand Down
1 change: 1 addition & 0 deletions jobsAdmin/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ func resurrectJobSummary(jm ste.IJobMgr) common.ListJobSummaryResponse {
switch jppt.TransferStatus() {
case common.ETransferStatus.NotStarted(),
common.ETransferStatus.FolderCreated(),
common.ETransferStatus.FolderExisted(),
common.ETransferStatus.Started(),
common.ETransferStatus.Restarted(),
common.ETransferStatus.Cancelled():
Expand Down
2 changes: 1 addition & 1 deletion ste/JobPartPlan.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
// dataSchemaVersion defines the data schema version of JobPart order files supported by
// current version of azcopy
// To be Incremented every time when we release azcopy with changed dataSchema
const DataSchemaVersion common.Version = 19
const DataSchemaVersion common.Version = 20

const (
CustomHeaderMaxBytes = 256
Expand Down
184 changes: 141 additions & 43 deletions ste/folderCreationTracker.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package ste

import (
"errors"
"fmt"
"net/url"
"strings"
Expand Down Expand Up @@ -31,17 +32,21 @@ func NewTransferFetcher(jobMgr IJobMgr) TransferFetcher {

// NewFolderCreationTracker creates a folder creation tracker taking in a TransferFetcher (typically created by NewTransferFetcher)
// A TransferFetcher is used in place of an IJobMgr to make testing easier to implement.
func NewFolderCreationTracker(fpo common.FolderPropertyOption, fetcher TransferFetcher) FolderCreationTracker {
switch fpo {
case common.EFolderPropertiesOption.AllFolders(),
common.EFolderPropertiesOption.AllFoldersExceptRoot():
func NewFolderCreationTracker(fpo common.FolderPropertyOption, fetcher TransferFetcher, fromTo common.FromTo) FolderCreationTracker {
switch {
// create a folder tracker when we're persisting properties
case fpo == common.EFolderPropertiesOption.AllFolders(),
fpo == common.EFolderPropertiesOption.AllFoldersExceptRoot(),
// create a folder tracker for destinations where we don't want to spam the service with create requests
// on folders that already exist
fromTo.To().IsFile(),
fromTo.To() == common.ELocation.BlobFS():
return &jpptFolderTracker{ // This prevents a dependency cycle. Reviewers: Are we OK with this? Can you think of a better way to do it?
fetchTransfer: fetcher,
mu: &sync.Mutex{},
contents: make(map[string]JpptFolderIndex),
unregisteredButCreated: make(map[string]struct{}),
fetchTransfer: fetcher,
mu: &sync.Mutex{},
contents: make(map[string]*JpptFolderTrackerState),
}
case common.EFolderPropertiesOption.NoFolders():
case fpo == common.EFolderPropertiesOption.NoFolders():
// can't use simpleFolderTracker here, because when no folders are processed,
// then StopTracking will never be called, so we'll just use more and more memory for the map
return &nullFolderTracker{}
Expand Down Expand Up @@ -69,15 +74,10 @@ func (f *nullFolderTracker) StopTracking(folder string) {

type jpptFolderTracker struct {
// fetchTransfer is used instead of a IJobMgr reference to support testing
fetchTransfer func(index JpptFolderIndex) *JobPartPlanTransfer
mu *sync.Mutex
contents map[string]JpptFolderIndex
unregisteredButCreated map[string]struct{}
}
fetchTransfer func(index JpptFolderIndex) *JobPartPlanTransfer
mu *sync.Mutex

type JpptFolderIndex struct {
PartNum PartNumber
TransferIndex uint32
contents map[string]*JpptFolderTrackerState
}

func (f *jpptFolderTracker) RegisterPropertiesTransfer(folder string, partNum PartNumber, transferIndex uint32) {
Expand All @@ -88,16 +88,34 @@ func (f *jpptFolderTracker) RegisterPropertiesTransfer(folder string, partNum Pa
return // Never persist to dev-null
}

f.contents[folder] = JpptFolderIndex{
state, present := f.contents[folder]

if !present {
state = &JpptFolderTrackerState{}
}

state.Index = &JpptFolderIndex{
PartNum: partNum,
TransferIndex: transferIndex,
}

// We created it before it was enumerated-- Let's register that now.
if _, ok := f.unregisteredButCreated[folder]; ok {
f.fetchTransfer(JpptFolderIndex{PartNum: partNum, TransferIndex: transferIndex}).SetTransferStatus(common.ETransferStatus.FolderCreated(), false)
// if our transfer already has a created status, we should adopt that.
if ts := f.fetchTransfer(*state.Index).TransferStatus(); ts == ts.FolderCreated() {
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

The comparison ts == ts.FolderCreated() is comparing the variable against a method call on itself, which will always return false. This should be ts == common.ETransferStatus.FolderCreated() to properly check if the transfer status is FolderCreated.

Suggested change
if ts := f.fetchTransfer(*state.Index).TransferStatus(); ts == ts.FolderCreated() {
if ts := f.fetchTransfer(*state.Index).TransferStatus(); ts == common.ETransferStatus.FolderCreated() {

Copilot uses AI. Check for mistakes.
state.Status = EJpptFolderTrackerStatus.FolderCreated()
} else {
// otherwise, we map onto it whatever we have. This puts the statuses in alignment.
switch state.Status {
case EJpptFolderTrackerStatus.FolderExisted():
f.fetchTransfer(*state.Index).SetTransferStatus(common.ETransferStatus.FolderExisted(), false)
case EJpptFolderTrackerStatus.FolderCreated():
f.fetchTransfer(*state.Index).SetTransferStatus(common.ETransferStatus.FolderCreated(), false)
}
}

delete(f.unregisteredButCreated, folder)
f.debugCheckState(*state)

if !present {
f.contents[folder] = state
}
}

Expand All @@ -109,32 +127,75 @@ func (f *jpptFolderTracker) CreateFolder(folder string, doCreation func() error)
return nil // Never persist to dev-null
}

if idx, ok := f.contents[folder]; ok &&
f.fetchTransfer(idx).TransferStatus() == (common.ETransferStatus.FolderCreated()) {
return nil
}
state, ok := f.contents[folder]

if ok {
if state.Index != nil {
ts := f.fetchTransfer(*state.Index).TransferStatus()

if _, ok := f.unregisteredButCreated[folder]; ok {
return nil
if ts == common.ETransferStatus.FolderCreated() ||
ts == common.ETransferStatus.FolderExisted() {
return nil // do not re-create an existing folder
}
} else {
if state.Status != EJpptFolderTrackerStatus.Unseen() {
return nil
}
}
}

err := doCreation()
if err != nil {
if err != nil && !errors.Is(err, common.FolderCreationErrorAlreadyExists{}) {
return err
}

if idx, ok := f.contents[folder]; ok {
// overwrite it's transfer status
f.fetchTransfer(idx).SetTransferStatus(common.ETransferStatus.FolderCreated(), false)
} else {
// A folder hasn't been hit in traversal yet.
// Recording it in memory is OK, because we *cannot* resume a job that hasn't finished traversal.
f.unregisteredButCreated[folder] = struct{}{}
if state == nil {
state = &JpptFolderTrackerState{}
}

if err == nil { // first, update our internal status, then,
state.Status = EJpptFolderTrackerStatus.FolderCreated()
} else if errors.Is(err, common.FolderCreationErrorAlreadyExists{}) {
state.Status = EJpptFolderTrackerStatus.FolderExisted()
}

if state.Index != nil {
// commit the state if needbe.
switch state.Status {
case EJpptFolderTrackerStatus.FolderCreated():
f.fetchTransfer(*state.Index).SetTransferStatus(common.ETransferStatus.FolderCreated(), false)
case EJpptFolderTrackerStatus.FolderExisted():
f.fetchTransfer(*state.Index).SetTransferStatus(common.ETransferStatus.FolderExisted(), false)
}
}

f.debugCheckState(*state)

Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

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

When a new state is created (line 153) for a folder that isn't already in f.contents, it needs to be stored in the map. After updating the state's Status field (lines 156-160), add f.contents[folder] = state before line 174 to persist the state. Without this, the state is lost and subsequent calls to CreateFolder for the same folder won't see the previous status, defeating the purpose of tracking folder creation.

Suggested change
f.contents[folder] = state

Copilot uses AI. Check for mistakes.
return nil
}

// debugCheckState validates that, if there is a plan file entry, that the plan file state matches the tracker's state.
// this should be called any time state gets used, to ensure it doesn't go out of sync.
func (f *jpptFolderTracker) debugCheckState(state JpptFolderTrackerState) {
if state.Index == nil {
return // nothing to do, current status is in table.
}

ts := f.fetchTransfer(*state.Index).TransferStatus()
passed := true

switch state.Status {
case EJpptFolderTrackerStatus.FolderCreated():
passed = ts == common.ETransferStatus.FolderCreated()
case EJpptFolderTrackerStatus.FolderExisted():
passed = ts == common.ETransferStatus.FolderExisted()
}

if !passed {
panic(fmt.Sprintf("internal folder state didn't match plan state: (internal: %v) (plan: %v)", state.Status, ts))
}
}

func (f *jpptFolderTracker) ShouldSetProperties(folder string, overwrite common.OverwriteOption, prompter common.Prompter) bool {
if folder == common.Dev_Null {
return false // Never persist to dev-null
Expand All @@ -151,12 +212,17 @@ func (f *jpptFolderTracker) ShouldSetProperties(folder string, overwrite common.
defer f.mu.Unlock()

var created bool
if idx, ok := f.contents[folder]; ok {
created = f.fetchTransfer(idx).TransferStatus() == common.ETransferStatus.FolderCreated()
} else {
// This should not happen, ever.
// Folder property jobs register with the tracker before they start getting processed.
panic(common.NewAzCopyLogSanitizer().SanitizeLogMessage("folder " + folder + " was not registered when properties persistence occurred"))

if state, ok := f.contents[folder]; ok {
if state.Index == nil {
// This should not happen, ever.
// Folder property jobs register with the tracker before they start getting processed.
panic(common.NewAzCopyLogSanitizer().SanitizeLogMessage("folder " + folder + " was not registered when properties persistence occurred"))
}

// status should, at this point, be aligned with the job plan status.
created = state.Status == EJpptFolderTrackerStatus.FolderCreated()
f.debugCheckState(*state)
}

// prompt only if we didn't create this folder
Expand Down Expand Up @@ -198,10 +264,42 @@ func (f *jpptFolderTracker) StopTracking(folder string) {
currentContents := ""

for k, v := range f.contents {
currentContents += fmt.Sprintf("K: %s V: %d\n", k, v)
currentContents += fmt.Sprintf("K: %s V: %v\n", k, v)
}

// double should never be hit, but *just in case*.
panic(common.NewAzCopyLogSanitizer().SanitizeLogMessage("Folder " + folder + " shouldn't finish tracking until it's been recorded\nCurrent Contents:\n" + currentContents))
}
}

// ===== Supporting Types =====

type JpptFolderTrackerStatus uint
type eJpptFolderTrackerStatus struct{}

var EJpptFolderTrackerStatus eJpptFolderTrackerStatus

// Unseen - brand new folder
func (eJpptFolderTrackerStatus) Unseen() JpptFolderTrackerStatus {
return 0
}

// FolderExisted - the folder was here before we got to it
func (eJpptFolderTrackerStatus) FolderExisted() JpptFolderTrackerStatus {
return 1
}

// FolderCreated - the folder was created by us.
func (eJpptFolderTrackerStatus) FolderCreated() JpptFolderTrackerStatus {
return 2
}

type JpptFolderIndex struct {
PartNum PartNumber
TransferIndex uint32
}

type JpptFolderTrackerState struct {
Index *JpptFolderIndex
Status JpptFolderTrackerStatus
}
Loading
Loading