Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

store: new API ApplyStagedLayer #1826

Merged
merged 4 commits into from
Feb 16, 2024

Conversation

giuseppe
Copy link
Member

@giuseppe giuseppe commented Feb 12, 2024

Add a race-condition-free alternative to using CreateLayer and ApplyDiffFromStagingDirectory, ensuring the store is locked for the entire duration while the layer is being created and populated.

Signed-off-by: Giuseppe Scrivano [email protected]

The relative patch for c/image is:

diff --git a/storage/storage_dest.go b/storage/storage_dest.go
index 88e492b7..51b30a64 100644
--- a/storage/storage_dest.go
+++ b/storage/storage_dest.go
@@ -149,7 +149,7 @@ func (s *storageImageDestination) Close() error {
 	}
 	for _, v := range s.diffOutputs {
 		if v.Target != "" {
-			_ = s.imageRef.transport.store.CleanupStagingDirectory(v.Target)
+			_ = s.imageRef.transport.store.CleanupStagedLayer(v)
 		}
 	}
 	return os.RemoveAll(s.directory)
@@ -669,11 +669,6 @@ func (s *storageImageDestination) commitLayer(index int, info addedLayerInfo, si
 			return false, fmt.Errorf("index %d out of range for configOCI.RootFS.DiffIDs", index)
 		}
 
-		layer, err := s.imageRef.transport.store.CreateLayer(id, parentLayer, nil, "", false, nil)
-		if err != nil {
-			return false, err
-		}
-
 		// let the storage layer know what was the original uncompressed layer.
 		flags := make(map[string]interface{})
 		flags[expectedLayerDiffIDFlag] = configOCI.RootFS.DiffIDs[index]
@@ -682,8 +677,15 @@ func (s *storageImageDestination) commitLayer(index int, info addedLayerInfo, si
 			Flags: flags,
 		}
 
-		if err := s.imageRef.transport.store.ApplyDiffFromStagingDirectory(layer.ID, diffOutput.Target, diffOutput, options); err != nil {
-			_ = s.imageRef.transport.store.Delete(layer.ID)
+		args := storage.ApplyStagedLayerOptions{
+			ID:          id,
+			ParentLayer: parentLayer,
+
+			DiffOutput:       diffOutput,
+			DiffOptions:      options,
+		}
+		layer, err := s.imageRef.transport.store.ApplyStagedLayer(args)
+		if err != nil {
 			return false, err
 		}

Copy link
Contributor

openshift-ci bot commented Feb 12, 2024

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: giuseppe

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

store.go Outdated Show resolved Hide resolved
store.go Outdated Show resolved Hide resolved
store.go Show resolved Hide resolved
store.go Outdated

StagingDirectory string
DiffOutput *drivers.DriverWithDifferOutput
DiffOptions *drivers.ApplyDiffWithDifferOpts
Copy link
Collaborator

Choose a reason for hiding this comment

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

Another opportunity to clean up that ApplyDiffWithDiffer and ApplyDiffFromStagingDirectory should have separate options types, so that callers aren’t tempted to set completely ignored options.

I guess that’s non-blocking…

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree the API can be improved, and this is a good chance. More in details, how would you improve this part?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I’m not sure I have the whole picture … All I really wanted here was to have separate ApplyDiffWithDifferOptions and ApplyDiffFromStagingDirectoryOptions, and to drop fields that are not relevant to either operation.

But looking at ApplyDiffOptions below, even that seems fairly invasive to do to the maximum extent.


Looking a bit further… A lot of this might be better as separate PRs, and probably not right now?

  • Is there any caller of ApplyDiffWithDiffer writing to a pre-existing layer? If not, maybe that can be just dropped. In the driver, the usingComposefs paths have already diverged, which seem like a good reason to either share more code (not in this PR!), or to remove the redundant code path entirely.
  • From a c/image perspective, we now have two “apply” functions which are ~opposites of each other, and it always takes me a bit to tell which one is “stage” and which one is “commit”. So something like s/ApplyDiffWithDiffer/StageChangesWithDiffer/ would be nice — but that also somewhat depends on the above.
  • Actually having ApplyDiffOptions inside ApplyDiffWithDifferOptions is not a good semantic match at all — ApplyDiffOptions all revolve around the ApplyDiffOptions.Diff stream, and that is completely ignored in those paths.
  • … and using the graphdriver.Differ interface to connect the overlay driver and the zstd deduplicator code seems like an imprecise fit as well; maybe that should be just a c-storage-private interface with no exposed methods, so that c/storage can change the mechanics over time.
  • as mentioned elsewhere, it would be convenient for CleanupStagingDirectory to consume DriverWithDifferOutput so that c/image doesn’t need to care about .Target at all

Copy link
Collaborator

Choose a reason for hiding this comment

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

A much more general, and much more vague, thought is that the “stage”+“commit” model might be interesting for non-chunked layers as well.

Right now, on the PutBlob path, c/image:

  • putBlobToPendingFile stores the input stream into a file, fully consuming it. (That must happen to validate digests.) That is fully parallel.
  • commitLayer extracts the file into a the graph driver’s layer. On some graph drivers (DM, VFS), that is inherently serial; in overlay, that is just a tar extraction which could, in principle, be fully parallel
  • creates the layer record, with parent links, etc. That is inherently serial, or maybe it could be parallel but it is anyway cheap enough not to worry about.

It seems potentially interesting to see whether the “extract tar” part could be parallelized — and whether it would be better. I can imagine that this part is I/O heavy enough that doing two of them would just slow things down; that probably needs building and measuring.

The "store stream” + “extract" + “commit" parts seem very similar to what the chunked path does in the “convert” case. OTOH “true chunked” input does’t have a stream in the first place…


But then, worrying about the traditional tar layers is backwards-looking. What would an ideal API for creating a composefs layer look like? I didn’t look into that and I have no idea. Would that be relevant for building the chunked one?

Copy link
Collaborator

Choose a reason for hiding this comment

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

… I think Dan would bite our head off if we worked on changing the traditional-tar API right now :)

store.go Outdated Show resolved Hide resolved
@mtrmac
Copy link
Collaborator

mtrmac commented Feb 12, 2024

Thanks for working on this!

@giuseppe giuseppe force-pushed the put-partial-layers branch 3 times, most recently from 0a76801 to 5e58781 Compare February 12, 2024 20:34
@giuseppe
Copy link
Member Author

@mtrmac thanks for the review.

I've addressed your comments, except one pending question: #1826 (comment)

@giuseppe giuseppe marked this pull request as ready for review February 13, 2024 08:03
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

Changing the API, after looking a bit more, seems to to require a bunch of fairly intrusive changes all at once… so I don’t think it is worth blocking this PR on them.

Or maybe you can see some way to make all of that simple.

layers.go Outdated Show resolved Hide resolved
store.go Outdated

StagingDirectory string
DiffOutput *drivers.DriverWithDifferOutput
DiffOptions *drivers.ApplyDiffWithDifferOpts
Copy link
Collaborator

Choose a reason for hiding this comment

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

I’m not sure I have the whole picture … All I really wanted here was to have separate ApplyDiffWithDifferOptions and ApplyDiffFromStagingDirectoryOptions, and to drop fields that are not relevant to either operation.

But looking at ApplyDiffOptions below, even that seems fairly invasive to do to the maximum extent.


Looking a bit further… A lot of this might be better as separate PRs, and probably not right now?

  • Is there any caller of ApplyDiffWithDiffer writing to a pre-existing layer? If not, maybe that can be just dropped. In the driver, the usingComposefs paths have already diverged, which seem like a good reason to either share more code (not in this PR!), or to remove the redundant code path entirely.
  • From a c/image perspective, we now have two “apply” functions which are ~opposites of each other, and it always takes me a bit to tell which one is “stage” and which one is “commit”. So something like s/ApplyDiffWithDiffer/StageChangesWithDiffer/ would be nice — but that also somewhat depends on the above.
  • Actually having ApplyDiffOptions inside ApplyDiffWithDifferOptions is not a good semantic match at all — ApplyDiffOptions all revolve around the ApplyDiffOptions.Diff stream, and that is completely ignored in those paths.
  • … and using the graphdriver.Differ interface to connect the overlay driver and the zstd deduplicator code seems like an imprecise fit as well; maybe that should be just a c-storage-private interface with no exposed methods, so that c/storage can change the mechanics over time.
  • as mentioned elsewhere, it would be convenient for CleanupStagingDirectory to consume DriverWithDifferOutput so that c/image doesn’t need to care about .Target at all

store.go Outdated

StagingDirectory string
DiffOutput *drivers.DriverWithDifferOutput
DiffOptions *drivers.ApplyDiffWithDifferOpts
Copy link
Collaborator

Choose a reason for hiding this comment

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

A much more general, and much more vague, thought is that the “stage”+“commit” model might be interesting for non-chunked layers as well.

Right now, on the PutBlob path, c/image:

  • putBlobToPendingFile stores the input stream into a file, fully consuming it. (That must happen to validate digests.) That is fully parallel.
  • commitLayer extracts the file into a the graph driver’s layer. On some graph drivers (DM, VFS), that is inherently serial; in overlay, that is just a tar extraction which could, in principle, be fully parallel
  • creates the layer record, with parent links, etc. That is inherently serial, or maybe it could be parallel but it is anyway cheap enough not to worry about.

It seems potentially interesting to see whether the “extract tar” part could be parallelized — and whether it would be better. I can imagine that this part is I/O heavy enough that doing two of them would just slow things down; that probably needs building and measuring.

The "store stream” + “extract" + “commit" parts seem very similar to what the chunked path does in the “convert” case. OTOH “true chunked” input does’t have a stream in the first place…


But then, worrying about the traditional tar layers is backwards-looking. What would an ideal API for creating a composefs layer look like? I didn’t look into that and I have no idea. Would that be relevant for building the chunked one?

store.go Outdated

StagingDirectory string
DiffOutput *drivers.DriverWithDifferOutput
DiffOptions *drivers.ApplyDiffWithDifferOpts
Copy link
Collaborator

Choose a reason for hiding this comment

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

… I think Dan would bite our head off if we worked on changing the traditional-tar API right now :)

@giuseppe giuseppe changed the title store: new API PutLayerFromStagingDirectory store: new API PutLayerFromStaging Feb 14, 2024
@rhatdan
Copy link
Member

rhatdan commented Feb 14, 2024

LGTM

@giuseppe giuseppe changed the title store: new API PutLayerFromStaging store: new API ApplyStagedLayer Feb 14, 2024
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

On second thought, I’m afraid this isn’t sufficient.

It is fine for a running system: it prevents other processes from observing the WIP layer.

But it doesn’t handle crashes sufficiently. For that, the layer metadata needs to be saved to disk with incompleteFlag (so that if we are recovering from a crash, we delete everything), and after the contents are set up, the flag is removed again.

(Or, hypothetically, we could first write the on-disk contents and only afterwards write the layer metadata?? But that would be a new unproven code path, and we would have to worry about re-creating a layer on top of previously partially-created files. Seems risky, when the other path is well-understood.)

Very roughly speaking, I think this can be done by applying the staged data from inside layerStore.create, around the place where applyDiffWithOptions is called for non-chunked layers.

store.go Outdated Show resolved Hide resolved
store.go Show resolved Hide resolved
@mtrmac
Copy link
Collaborator

mtrmac commented Feb 14, 2024

The API design LGTM.

For the record:

The relative patch for c/image is:

diff --git a/storage/storage_dest.go b/storage/storage_dest.go
index 88e492b7..51b30a64 100644
--- a/storage/storage_dest.go
+++ b/storage/storage_dest.go

+		layer, err := s.imageRef.transport.store.ApplyStagedLayer(args)
+		if err != nil {

This path in c/image also needs to check for, and succeed with, ErrDuplicateID. And it would be convenient, and consistent with PutLayer, for the new function to return a layer value with the duplicate object in this case.

store.go Outdated Show resolved Hide resolved
store.go Outdated Show resolved Hide resolved
@giuseppe
Copy link
Member Author

On second thought, I’m afraid this isn’t sufficient.

It is fine for a running system: it prevents other processes from observing the WIP layer.

But it doesn’t handle crashes sufficiently. For that, the layer metadata needs to be saved to disk with incompleteFlag (so that if we are recovering from a crash, we delete everything), and after the contents are set up, the flag is removed again.

(Or, hypothetically, we could first write the on-disk contents and only afterwards write the layer metadata?? But that would be a new unproven code path, and we would have to worry about re-creating a layer on top of previously partially-created files. Seems risky, when the other path is well-understood.)

Very roughly speaking, I think this can be done by applying the staged data from inside layerStore.create, around the place where applyDiffWithOptions is called for non-chunked layers.

you are right, we need to use the incompleteFlag. I've pushed a new version where I set the incompleteFlag, barely tested. I'll work more on it through the day

@giuseppe
Copy link
Member Author

@mtrmac what do you think of the last version?

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

I’d rather prefer if the incompleteFlag remained an internal implementation detail of layers.go.

@giuseppe
Copy link
Member Author

I’d rather prefer if the incompleteFlag remained an internal implementation detail of layers.go.

moved the incompleteFlag usage to layers.go

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

This is a half-way step, but the partialoption is still basically “leave the layer incomplete” and/or “I promise to callapplyDiffFromStagingDirectorylater” (with neither effect documented), withstore.go` being responsible for that.

It seems to me

	if diff != nil {
		if size, err = r.applyDiffWithOptions …
+	else if staged != nil {
+		if size, err = r.applyDiffFromStagingDirectory …
	else { …

should be possible and and not too invasive a change.

layers.go Outdated Show resolved Hide resolved
@giuseppe giuseppe force-pushed the put-partial-layers branch 2 times, most recently from 34fbf02 to 4c51bea Compare February 15, 2024 18:28
@giuseppe
Copy link
Member Author

pushed a new version, I moved the applyDiffFromStagingDirectory() call inside create()

Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

ACK.

layers.go Outdated Show resolved Hide resolved
store.go Outdated Show resolved Hide resolved
this is needed by the following commit.

Signed-off-by: Giuseppe Scrivano <[email protected]>
enforce that the stagingDirectory must have the same value as the
diffOutput.Target variable.  It allows to simplify the internal API.

Signed-off-by: Giuseppe Scrivano <[email protected]>
Add a race-condition-free alternative to using CreateLayer and
ApplyDiffFromStagingDirectory, ensuring the store is locked for the
entire duration while the layer is being created and populated.

Signed-off-by: Giuseppe Scrivano <[email protected]>
It uses the diff output as input and callers are not expected to know
about the Target directory.

Signed-off-by: Giuseppe Scrivano <[email protected]>
Copy link
Collaborator

@mtrmac mtrmac left a comment

Choose a reason for hiding this comment

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

/lgtm

Thanks!

@openshift-ci openshift-ci bot added the lgtm label Feb 16, 2024
@openshift-merge-bot openshift-merge-bot bot merged commit 6f63bc4 into containers:main Feb 16, 2024
18 checks passed
@giuseppe
Copy link
Member Author

@mtrmac the related change in c/image: containers/image#2301

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants