-
Notifications
You must be signed in to change notification settings - Fork 18
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
Basic implementation of the executor and the run command #6
Conversation
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.
That looks great! Did the first run on the PR and left some initial thoughts. Will open in IntelliJ tomorrow and will run some things locally to test more.
return nil | ||
} | ||
|
||
func createAgentVolume( |
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.
Let's move this method after GetAgentVolume
where it's used. It's easier to read when things are in order of usage.
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.
Do you have any convention or a coding style document in mind that specifically addresses this? 🤔
It seems that by changing this we're essentially introducing our own coding style which will complicate drive-by contributions (simply because it needs to be communicated via a written document, lint rule or a PR comment).
On top of that, this adds unnecessary cognitive load to the developer when making a change (due to ambiguities in determining which function should follow which), yet adds a little benefit with the advent of modern IDE's that make sure there's no practical difference between any of the approaches.
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.
That's just how I always wrote code. Don't remember who taught me that but just check how GetAgentVolume
ends:
return createAgentVolume(ctx, cli, containerNameTemporary, containerNameFinal, volumeName)
It will be very nice for human readability to see createAgentVolume
right after it. We can investigate if golangci-lint has rule for it and if not we can try 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.
Another point here is to declare public functions first in the file.
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.
See 8b1d285.
internal/executor/build/build.go
Outdated
tasks map[int64]*Task | ||
|
||
// A mutex to guarantee safe access to this build from both the "main loop" and gRPC server handlers | ||
Mutex sync.Mutex |
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 haven't seen any build access pattern that was not thread safe. I think it will be safe to remove Mutex. Did I miss something? Once we have parallel tasks and a fancy cmd UI then we'll need some synchronization there.
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 don't like this either, but this is not just about the locking per se, more about the memory barriers and visibility.
Without following the intricate rules of the Go memory model (which is not recommended, see "Advice" section) or implementing an explicit synchronization with e.g. sync.Mutex
, there's no guarantee that we'll see up-to-date task status values in Executor.Run()
, since gRPC server that we also run handles requests and updates status values in separate goroutines, which easily translates to "runs in a separate thread", which in turn can easily cause problems.
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.
Yes, task's status is the only thing that changes and CLI need to have updated value for figuring out which task to run next. We can just use some atomic in order to do the updates (make status private and use getter and setter for the atomic operation). What do you think?
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 thought about wrapping it in an interface method too, but don't you think that in the near future more fields will show up (as we approach functional completeness from the protocol PoV), and it would basically end up with the same mutex wraps?
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 just don't like concurrency primitives except channels and atomics. In most of the cases use of a Mutex overcomplicates things. I'll think how we can re-architecture things like instance
, executor
and build
. 🤔
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.
On the second though it seems we'll have serialization of the status update by nature of how things are executed at the moment: a status is updated before container is killed and the access to the task status happens after container is cleaned up in order to start a new task.
I'm currently also don't like how instance
is responsible for executing itself. It simplifies things when we are running one task at a time but with the parallel execution we'll need to figure out some sort of instance manager that will be aware of local resources available to the CLI and run as many tasks in parallel as possible and there will need synchronization. Probably with some sort of a channel with task updates.
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.
On the second though it seems we'll have serialization of the status update by nature of how things are executed at the moment: a status is updated before container is killed and the access to the task status happens after container is cleaned up in order to start a new task.
You're right, but The Go Memory Model document clearly recommends against this:
Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.
To serialize access, protect the data with channel operations or other synchronization primitives such as those in the sync and sync/atomic packages.
If you must read the rest of this document to understand the behavior of your program, you are being too clever.
Don't be clever.
I'll implement the getter/setter methods as discussed above, and if you will keep the history while merging this, we will be able to reference the sync.Mutex
solution any time in the future (props to Git).
I'm currently also don't like how
instance
is responsible for executing itself.
Is it? As far as I can see, it is being executed by the Executor
by calling the Run()
method on an Instance
.
It simplifies things when we are running one task at a time but with the parallel execution we'll need to figure out some sort of instance manager that will be aware of local resources available to the CLI and run as many tasks in parallel as possible and there will need synchronization. Probably with some sort of a channel with task updates.
I agree that an instance manager makes sense in the future, but hey, this is a basic implementation just so we can start moving forward!
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.
Ok, so we are agreeing on moving the mutex to task and having getter / setter? I'll be OK with that and we can revisit it once we'll think about the parallel execution.
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.
Added sync.Mutex
-wrapped getter and setter for the task's state field in a9a2096.
Parallelization can be enabled once we'll parallelize the Docker itself too (and this needs to be done on same host, since we're using bind mounts).
This was fixed on the Docker side since Docker Desktop Community 2.3.0.2[1]. [1]: https://docs.docker.com/docker-for-mac/release-notes/#docker-desktop-community-2302 Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
A missing piece for the previous commit. Co-authored-by: Fedor Korotkov <fedor.korotkov@gmail.com>
Turns out there's a difference between -p and -parallel flags[1][2]: >Note that -parallel only applies within a single test binary. >The 'go test' command may run tests for different packages >in parallel as well, according to the setting of the -p flag >(see 'go help build'). [1]: https://twitter.com/mitchellh/status/900391039252353024 [2]: https://golang.org/cmd/go/#hdr-Testing_flags
1. The only field that's modified in a concurrent fashion right now is the Task.status field. 2. Build fields that were previously protected by sync.Mutex are only modified before RPC server goroutine starts, which is a synchronization point according to The Go Memory Model[1]. Also see discussion in #6. [1]: https://golang.org/ref/mem
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.
Left a few nit picks. Overall looking excellent! 💪
internal/executor/build/task.go
Outdated
ProtoTask *api.Task | ||
|
||
// A mutex to guarantee safe accesses from both the main loop and gRPC server handlers | ||
Mutex sync.Mutex |
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.
If we are going the mutex way let's use RWMutex
here.
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.
See dce79d7.
return nil | ||
} | ||
|
||
func createAgentVolume( |
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.
Another point here is to declare public functions first in the file.
Otherwise on Windows backslashes will be used and the agent or some other command will fail to start.
With these changes you can try to self-build the CLI
go run cmd/cirrus/main.go run -v
on macOS.Some things that still clearly need to be done:
.cirrus.yml
host.docker.internal
work on Linux