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

Merge winners with participants #3042

Merged
merged 8 commits into from
Oct 10, 2024
Merged

Conversation

sunce86
Copy link
Contributor

@sunce86 sunce86 commented Oct 8, 2024

Description

Extracted from https://github.com/cowprotocol/services/pull/2980/files

Merges participants and winners into a single vector, which is much easier to work with and forward around. Also reduces quite a few roundtrips when iterating over participants to determine winning and non-winning orders etc.

Winner selection logic is just moved into Runloop::competition function so that Participant could become a main struct we work with and try to even remove RawParticipant with refactors in the future, or at least make it local to competition only.

Changes

  • Moved Participants into domain
  • Participants now contain winner to determine if it's a winner and allowed to settle.

How to test

Existing e2e tests.

@sunce86 sunce86 self-assigned this Oct 8, 2024
@sunce86 sunce86 requested a review from a team as a code owner October 8, 2024 16:03
// Pick winners for execution
let winners = self.select_winners(&solutions);
if winners.is_empty() {
tracing::info!("no winners for auction");
Copy link
Contributor

Choose a reason for hiding this comment

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

Would still be good to keep this log around IMO.

Copy link
Contributor Author

@sunce86 sunce86 Oct 9, 2024

Choose a reason for hiding this comment

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

Currently, if there are no valid solutions, we have a log for that.

If there is at least one valid solution, there is automatically at least one winner (and it's always a first one), so it can't happen that we have no winners in this case.

crates/autopilot/src/run_loop.rs Outdated Show resolved Hide resolved
crates/autopilot/src/domain/competition/mod.rs Outdated Show resolved Hide resolved
crates/autopilot/src/run_loop.rs Outdated Show resolved Hide resolved
crates/autopilot/src/run_loop.rs Outdated Show resolved Hide resolved
crates/autopilot/src/run_loop.rs Show resolved Hide resolved
@@ -91,6 +91,35 @@ impl Score {
}
}

#[derive(Clone)]
pub struct RawParticipant {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure we gain much from this extra type. Instead we could initialize Participant with winner: false and have a second step that sets the value to true for the winners.

Copy link
Contributor Author

@sunce86 sunce86 Oct 9, 2024

Choose a reason for hiding this comment

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

In general, using the same struct to keep two or more different states of the object is an invitation for bugs to join the party (as we don't know which code now and in the future will use the object before second step).

If code duplication is issue here, I would rather have

#[derive(Clone)]
pub struct Participant {
    inner: RawParticipant,
    winner: bool,
}

impl Participant {
    pub fn driver(&self) -> &Arc<infra::Driver> {
        &self.inner.driver
    }

    pub fn solution(&self) -> &Solution {
        &self.inner.solution
    }

    pub fn is_winner(&self) -> bool {
        self.winner
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I am also more inclined not to deal with mutating objects. In my exp, that often results in errors and more complex code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

refactored according to example to remove code duplication

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't really see how turning RawParticipant into a Participant (i.e. adding the winner field) should not be considered mutating objects. Before Dusan introduced the accessor methods both solutions were equally prone to misuse because you could just change the public winner field of the Participant anyway.
Is the concept of a RawParticipant really so important that we should have a domain object for it?

I really want to avoid introducing more and more types which seem the same but are not quite the same. Otherwise you'll have the mental overhead of having to know which subtle detail is different in this particular version of the type.
WDYT about this to reduce the ways mutation can lead to issues (again I feel like what we did with the 2 types should also be considered mutating). That way you can only mutate the structs once in a predictable manner that makes sense in the domain:

#[derive(Clone)]
pub struct Participant {
    solution: Solution,
    driver: Arc<infra::Driver>,
    winner: bool,
}

impl Participant {
    pub fn driver(&self) -> &Arc<infra::Driver> {
        &self.inner.driver
    }

    pub fn solution(&self) -> &Solution {
        &self.inner.solution
    }

    pub fn is_winner(&self) -> bool {
        self.winner
    }
    
    pub fn mark_as_winner(&mut self) {
        self.is_winner = true;
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

If you are really adamant about type safety (i.e. having 2 types) I would at least make the names of the 2 structs more obvious: UnrankedParticipant and (Ranked)Participant.

Copy link
Contributor

@MartinquaXD MartinquaXD Oct 10, 2024

Choose a reason for hiding this comment

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

We could even play around with the typestate pattern. Although this is admittedly quite a lot of boiler plate code to achieve a simple thing and should probably even have it's own file to limit the scope of the new state types:

struct Participant<State> {
    solution: Solution,
    driver: Arc<infra::Driver>,
    state: State,
}

struct Unranked;
struct Ranked {
    is_winner: bool
}

impl <T> Participant<T> {
    pub fn solution(&self) -> &Solution {
        &self.solution
    }

    pub fn driver(&self) -> &Arc<infra::Driver> {
        &self.driver
    }
}

impl Participant<Unranked> {
    pub fn new(solution: Solution, driver: Arc<infra::Driver>) -> Self {
        Self {
            solution,
            driver,
            state: Unranked
        }
    }

    pub fn rank(self, is_winner: bool) -> Participant<Ranked> {
        Participant::<Ranked> {
            state: Ranked {
                is_winner,
            },
            solution: self.solution,
            driver: self.driver
        }
    }
}

impl Participant<Ranked> {
    pub fn is_winner(&self) -> bool {
        self.state.is_winner
    }
}

Copy link
Contributor Author

@sunce86 sunce86 Oct 10, 2024

Choose a reason for hiding this comment

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

The biggest concern would be if we use non-initialized is_winner while assuming it's initialized. I think I would be fine with initializing in two steps, if the whole capturing of solve response and competition is done in a single step, for example, if we encapsulate that in a Competition::new() constructor or something.

But I found you typestate pattern most adequate for this use case so I took it. 👍

@sunce86 sunce86 requested a review from MartinquaXD October 9, 2024 10:25
Copy link
Contributor

@m-lord-renkse m-lord-renkse left a comment

Choose a reason for hiding this comment

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

Do we plan to add e2e tests for this change? 🤔

crates/autopilot/src/domain/competition/mod.rs Outdated Show resolved Hide resolved
crates/autopilot/src/domain/competition/mod.rs Outdated Show resolved Hide resolved
@@ -522,7 +528,7 @@ impl RunLoop {

/// Runs the solver competition, making all configured drivers participate.
/// Returns all fair solutions sorted by their score (best to worst).
async fn competition(&self, auction: &domain::Auction) -> Vec<Participant> {
async fn competition(&self, auction: &domain::Auction) -> Vec<competition::Participant> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, can you add unit test for these changes? it is not trivial code and small bug could be hidden 🙏

Copy link
Contributor Author

@sunce86 sunce86 Oct 9, 2024

Choose a reason for hiding this comment

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

We did not envision implementing unit tests for the runloop functionality (although I agree it could be useful for these small logically interesting parts).

For now I am happy if the changes do not break the current setup (running with 1 winner) which I regularly check on staging.

Once the whole feature is finished, I think having complete e2e test would be useful before activating the feature gradually on each network.

Copy link
Contributor

@m-lord-renkse m-lord-renkse left a comment

Choose a reason for hiding this comment

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

LGTM

@sunce86 sunce86 added the E:6.2 Time to Happy Moo See https://github.com/cowprotocol/pm/issues/77 for details label Oct 10, 2024
@sunce86 sunce86 enabled auto-merge (squash) October 10, 2024 12:56
@sunce86 sunce86 merged commit 5dc5029 into main Oct 10, 2024
11 checks passed
@sunce86 sunce86 deleted the merge-winners-with-participants branch October 10, 2024 13:01
@github-actions github-actions bot locked and limited conversation to collaborators Oct 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
E:6.2 Time to Happy Moo See https://github.com/cowprotocol/pm/issues/77 for details
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants