-
Notifications
You must be signed in to change notification settings - Fork 276
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
pkg/oci: Use a file lock to avoid concurrency issues #3842
Conversation
596036f
to
be9d61d
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is another exported function from the oci.go
file: GetContentFromDescriptor
I think it would also need the locking, right?
While thinking about it: Do we want to have a timeout while we wait on the lock? |
be9d61d
to
616f249
Compare
Good idea. I used the TryLock with a timeout. |
I wonder if the 5-second timeout is too short: pulling an OCI image on a slow network could take more than 5 seconds. Another instance of ig might want to wait for longer before giving up. Alternatively, is it possible to download a large image in a temporary location before saving it into the OCI store? That might minimize the lock time. |
We could pull it to a temporary in memory store. And then "copy" that to the filesystem |
616f249
to
3eebf1f
Compare
Good point about pulling the image. The main issue is to have concurrent writes/reads to the index.json file. I pushed a different version that uses |
3eebf1f
to
6742a83
Compare
5bd1145
to
75d3f5a
Compare
I had to change this again. I've switched to using different instances of I'm aware of the limitations of this implementation: (1) ig can't be run when ig-k8s or ig daemon is running and (2) multiple instances of ig running at the same time will have big contention. Unfortunately, I couldn't find a better solution, and according to oras-project/oras-go#472 (comment) "Multi-process support is also another non-trivial work and it is not on the roadmap of oras-go as it requires an atomicity source such as databases, file systems with O_EXCL support, and etc.", so this PR is a kind of best-effort approach for now. |
bc32c07
to
0fd8e32
Compare
cmd/ig/daemon.go
Outdated
if err := oci.Lock(context.Background()); err != nil { | ||
return fmt.Errorf("locking OCI storage: %w", err) | ||
} | ||
defer oci.Unlock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you want to lock the daemon?
If daemon A pull an image, it has the lock, then daemon B should wait for the lock to be released before listing all images.
So, only operations need to be locked.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please see #3842 (comment) with some reasoning about it. In other words, what you're describing is the ideal case, but it's very difficult to implement (at least, I haven't found a solution for it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not like "big kernel lock"-like solution.
Particularly one problem I see here is we cannot run both ig-k8s and ig at the same time, which is strange because you could use ig daemon for another machine and wanting to run ig on your local one.
We already contributed to oras-go, I would reach them and ask whether they see big blockers toward this.
Having a good solution upstream would be better for everyone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not like "big kernel lock"-like solution.
I agree. However it doesn't seem to be trivial and this PR is just preventing the user from running something racy, like ig-k8s and ig on the same machine.
We already contributed to oras-go, I would reach them and ask whether they see big blockers toward this.
Having a good solution upstream would be better for everyone.
I'll reach to them to see if they have any ideas on how to implement it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did some research (ref links on PR description) and updated this PR to use a retry approach. I've been testing it and it seems to be working fine. There is one test failing, but I'm confident it can be fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is one test failing, but I'm confident it can be fixed.
It should be fixed by the latest push.
6e6728a
to
49f518b
Compare
6527364
to
71b610a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have some comments on the try approach.
Can you please open a dedicated PR for the first three commits? They are not related to the problem you are trying to solve here and we can merge them as is right now.
Done in #3919 |
3472437
to
68aebb3
Compare
68aebb3
to
4cdc031
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for fixing this finally
4cdc031
to
0b1bfdc
Compare
0b1bfdc
to
4262ee7
Compare
var errRetry = errors.New("retry") | ||
|
||
// ErrRetryLimitExceeded indicates the function was retry too many times. | ||
var ErrRetryLimitExceeded = errors.New("retry limit exceeded") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var ErrRetryLimitExceeded = errors.New("retry limit exceeded") | |
var errRetryLimitExceeded = errors.New("retry limit exceeded") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's fine to have this public. Callers could use it.
if err := ociStore.Tag(context.TODO(), targetDescriptor, dst.String()); err != nil { | ||
return nil, fmt.Errorf("tagging image: %w", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does Tag
fail if it is already tagged by a previous attempt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears to be idempotent:
$ sudo ig image tag trace_open foo
$ sudo ig image tag trace_open foo
$ sudo ig image tag trace_open foo
$ echo $?
0
pkg/oci/oci.go
Outdated
if err != nil { | ||
return | ||
} | ||
err = ociStore.saveIndexWithLock() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function calls ociStore.GC(ctx)
line 608.
But the documentation of GC
says:
// GC removes garbage from Store. Unsaved index will be lost. To prevent unexpected
// loss, call SaveIndex() before GC or set AutoSaveIndex to true.
With this PR, SaveIndex() is no longer called before GC().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. I've read this comment but I didn't get the idea before. I updated it to call saveIndexWithLock
before GC(). I think GC() can still be racy at this point, but I'll explicitly ignore this for now.
4262ee7
to
8352606
Compare
Invoking multiple instances of the ig binary on parallel causes issues as the oras library doesn't provide any multi process syncronization guarantees on the index.json file. This commit fixes that by using a file lock and some logic to detect when this file has been updated by another process. Specifically, this commit:: - Disables AutoSaveIndex to have control over when the index.json file is written. - Uses a file lock to be sure only one process is writing to the index.json file at a time. - Adds some logic to retry an operation when the index.json file has been updated by another process. Different alternatives were considered: - big lock: It caused issues as the critical section was too big and different instances of ig timeout while running gadgets. - merge index: Instead of retry the whole operation when the index.json file was written by another process, we could merge it with the changes. However it's difficult to implement as it'll need the oras library to provide a list of changes on the index to apply. - use a db to store the index: It probably requires a lot of changes to the oras library. Signed-off-by: Mauricio Vásquez <[email protected]>
Signed-off-by: Francis Laniel <[email protected]>
Signed-off-by: Francis Laniel <[email protected]>
8352606
to
dbabdd9
Compare
I've seen a snapshot process fail twice here, let's merge it and monitor to see if it continues happening. |
Invoking multiple instances of the ig binary on parallel causes issues
as the oras library doesn't provide any multi process syncronization
guarantees on the index.json file. This commit fixes that by using a
file lock and some logic to detect when this file has been updated by
another process.
Specifically, this commit:
is written.
index.json file at a time.
updated by another process.
Different alternatives were considered:
different instances of ig timeout while running gadgets.
file was written by another process, we could merge it with the changes.
However it's difficult to implement as it'll need the oras library to
provide a list of changes on the index to apply.
the oras library.
Testing
All the cases described here work fine with this PR, but fail before it
Multiple ig instances run on parallel
Try executing
ig image
commands at the same time, like building multiple gadgets and listing all gadgets:ig build on parallel
terminal 1:
terminal 2
Running multiple gadgets at the same time with the oci store is empty
$ sudo rm -rf /var/lib/ig $ sudo ig run trace_open & sudo ig run trace_exec [1] 139772 RUNTIME.CONTAINERNAME COMM PID TID FD FNAME MODE ERROR RUNTIME.CONTAINERNAME COMM PID TID ARGS ERRO
ig-k8s/ig daemon and ig run at the same time
Let's start with an empty oci store
Then, run ig daemon
$ sudo ig daemon INFO[0000] starting Inspektor Gadget daemon at "unix:///var/run/ig/ig.socket" WARN[0000] reading config: open /root/.ig/config.yaml: no such file or directory WARN[0000] no TLS configuration provided, communication between daemon and CLI will not be encrypted
On another terminal, try to run a gadget without pulling the image:
Pull the image with ig
Running the gadget again works:
It means
ig daemon
has visibility on the changes make by ig to the store without having to restart it.Fixes #3554
Ref oras-project/oras-go#472 (comment)
Ref oras-project/oras-go#286
Ref ratify-project/ratify#1110
Ref opencontainers/umoci#216 (comment)
Ref opencontainers/umoci#146