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

EVG-13846 EVG-13946 Mainline commits query and graphql resolver. #4663

Merged
merged 50 commits into from
May 25, 2021

Conversation

khelif96
Copy link
Contributor

@khelif96 khelif96 commented May 7, 2021

This is the main query for the mainline commits page.

This supports most of the core features of the mainline commits flow.

  • Fetching both activated and unactivated versions
    • Activated Versions are expanded and included extra metadata such as variants,
    • Unactivated versions do not need to have nor include this extra metadata
  • Filtering by task status
  • Filter by task name/display name
  • Filtering by task status
  • Pagination
    • This is a little difficult and i was hoping for some suggestions on how to achieve it.
    • Ideally i want to handle the pagination on the db side of things using a $limit and $skip
    • The complexity surrounds how we want to handle activated vs unactivated versions.
      • Activated versions are expanded and should count towards the limit of 7 activated versions on the screen at once.
      • Unactivated versions are collapsed and should not count towards the limit.
      • Ideally we could do a search with a limit of 7 active versions. and all the unactivated versions would not count towards this limit but still be returned in the result. And the skip function would hopefully work the same way. Skip the first n*7 activated results + all unactivated results that fall within.
      • If it's possible to do it in the db we could solve the issue of over fetching data. Alternatively we could do this logic in the resolver And intentionally over-fetch data and hope we have enough results to populate the page. But pagination logic will get pretty complicated, We wouldn't be able to simply pass in an arbitrary page number we would need some way to keep track of how many versions we skipped in the resolver somehow.

This notably does not include the ability to filter by test results. After speaking with @julianedwards it seems like it might be considerably more difficult to implement than expected so that is being held off for a later discussion.

@@ -242,8 +242,8 @@ func (tc *DBTaskConnector) GetManifestByTask(taskId string) (*manifest.Manifest,

// FindTasksByVersion gets all tasks for a specific version
// Results can be filtered by task name, variant name and status in addition to being paginated and limited
func (tc *DBTaskConnector) FindTasksByVersion(versionID string, statuses []string, baseStatuses []string, variant string, taskName string, page, limit int, fieldsToProject []string, sorts []task.TasksSortOrder) ([]task.Task, int, error) {
tasks, total, err := task.GetTasksByVersion(versionID, sorts, statuses, baseStatuses, variant, taskName, page, limit, fieldsToProject)
func (tc *DBTaskConnector) FindTasksByVersion(versionID string, statuses []string, baseStatuses []string, variants []string, taskNames []string, page, limit int, fieldsToProject []string, sorts []task.TasksSortOrder) ([]task.Task, int, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry to ask this of you because it probably should've been done before, but this is getting really unwieldy; having this many inputs of the same type so close together introduces a lot of room for programmer error, and makes code very difficult to read. Would you mind refactoring this to take in a filterOptions struct or something, that contains the fields between statuses -> sort? (This also means you should just be able to pass in a blank filterOptions struct when you don't want to filter, which would be nice)

graphql/util.go Outdated
ets = append(ets, &at)
}
result.ExecutionTasksFull = ets
func GenerateBuildVariants(ctx context.Context, sc data.Connector, versionId string, searchVariants []string, searchTasks []string, statuses []string) ([]*GroupedBuildVariant, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this function can just be private (otherwise I'd probably ask that input is given with a struct rather than a collection of string maps, and a function header is provided)

graphql/util.go Outdated
}
tasks, _, err := sc.FindTasksByVersion(versionId, statuses, []string{}, searchVariants, searchTasks, 0, 0, []string{}, defaultSort)
if err != nil {
return nil, InternalServerError.Send(ctx, fmt.Sprintf("Error getting tasks for patch `%s`: %s", versionId, err))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want this to be err.Error() explicitly?

graphql/util.go Outdated
if err != nil {
return nil, InternalServerError.Send(ctx, fmt.Sprintf("Error getting tasks for patch `%s`: %s", versionId, err))
}
for _, task := range tasks {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: avoid package names as variable names (in case we ever need to use the task collection in this function)

graphql/resolvers.go Show resolved Hide resolved
Tasks []*model.APITask `json:"tasks"`
type MainlineCommit struct {
Version *model.APIVersion `json:"version"`
VersionMeta *VersionMeta `json:"versionMeta"`
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we call this versionMetadata instead? "meta" doesn't really mean anything and I'm not sure there's a reason to shorten it. Also, I'm wondering why you want a separate struct for metadata, as opposed to just filling out the necessary fields in the APIVersion. Is this to differentiate activated from not activated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes this is so the buildVariant resolver isn't used on non activated versions. Since the types are different we're able to be explicit about what fields activated and non activated versions accept. I may try to build out a better type for this though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried to implement something like this.

type MainlineCommit {
  version: Version
}

union Version = UnactivatedVersion | ActivatedVersion

type UnactivatedVersion {
  id: String!
  createTime: Time!
  message: String!
  revision: String!
  author: String!
  status: String!
  order: Int!
  repo: String!
  project: String!
  branch: String!
  requester: String!
  activated: Boolean
}

type ActivatedVersion {
  startTime: Time!
  finishTime: Time!
  buildVariants(options: BuildVariantOptions): [GroupedBuildVariant]
}

But a bug in gqlgen does not allow me to generate the types for union types and interfaces. So i think this is the best we can do for now.

model/version.go Outdated
if err := db.Aggregate(VersionCollection, pipeline, &res); err != nil {
return nil, errors.Wrapf(err, "error aggregating versions and builds")
}
if len(res) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

This if statement seems unnecessary

model/version.go Outdated
res := []Version{}

if err := db.Aggregate(VersionCollection, pipeline, &res); err != nil {
return nil, errors.Wrapf(err, "error aggregating versions and builds")
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't get builds, right? Since it's only aggregating on the VersionCollection. Do you want it to also be getting builds?

model/version.go Show resolved Hide resolved
@khelif96 khelif96 requested a review from ablack12 May 12, 2021 21:23
@@ -824,7 +824,17 @@ func (r *patchResolver) TaskStatuses(ctx context.Context, obj *restModel.APIPatc
defaultSort := []task.TasksSortOrder{
{Key: task.DisplayNameKey, Order: 1},
}
tasks, _, err := r.sc.FindTasksByVersion(*obj.Id, []string{}, []string{}, "", "", 0, 0, []string{task.DisplayStatusKey}, defaultSort)
opts := data.TaskFilterOptions{
Copy link
Contributor

Choose a reason for hiding this comment

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

So you don't need to specify any of the fields you don't want to use; the defaults are the same as what you're specifying.

Copy link
Contributor

Choose a reason for hiding this comment

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

So what I meant here was that you should just say:
opts := data.TaskFilterOptions{Sorts: defaultSort} rather than specifying the fields that you don't need (same below).

model/version.go Show resolved Hide resolved
model/version.go Outdated Show resolved Hide resolved
model/version.go Outdated Show resolved Hide resolved
model/version.go Outdated Show resolved Hide resolved
var match bson.M = bson.M{}

// Allow searching by either variant name or variant display
if variant != "" {
if len(variants) > 0 {
variantsAsRegex := strings.Join(variants[:], "|")
Copy link
Contributor

Choose a reason for hiding this comment

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

What is this [:] for?

Copy link
Contributor

Choose a reason for hiding this comment

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

^

graphql/resolvers.go Outdated Show resolved Hide resolved
var mainlineCommits MainlineCommits
activatedVersionCount := 0
for _, v := range versions {
if activatedVersionCount == *options.Limit {
Copy link
Contributor

Choose a reason for hiding this comment

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

preference for FromIntPointer since it's safer
Do we set options.Limit to the default anywhere here or is this potentially just 0?

Copy link
Contributor

Choose a reason for hiding this comment

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

This hasn't been answered/updated ^

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You are right i assumed gqlgen would have automatically applied a default for this field based off of us supplying a default value in the schema definition. But that doesn't appear to be the case.

graphql/resolvers.go Show resolved Hide resolved
model/version.go Outdated
Activated bool `json:"activated"`
}

func GetMainlineCommitVersionsWithOptions(projectName string, opts MainlineCommitVersionOptions) ([]Version, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we write a unit test for this? I'd like to know if you have a Version inserted with Activated not set, if it gets included in the Activated: false match; otherwise if Activated is false we likely want to query both "false and unset"

@khelif96 khelif96 requested a review from ablack12 May 17, 2021 13:30
@@ -824,7 +824,17 @@ func (r *patchResolver) TaskStatuses(ctx context.Context, obj *restModel.APIPatc
defaultSort := []task.TasksSortOrder{
{Key: task.DisplayNameKey, Order: 1},
}
tasks, _, err := r.sc.FindTasksByVersion(*obj.Id, []string{}, []string{}, "", "", 0, 0, []string{task.DisplayStatusKey}, defaultSort)
opts := data.TaskFilterOptions{
Copy link
Contributor

Choose a reason for hiding this comment

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

So what I meant here was that you should just say:
opts := data.TaskFilterOptions{Sorts: defaultSort} rather than specifying the fields that you don't need (same below).


activatedVersions, err := model.GetMainlineCommitVersionsWithOptions(projectId, opts)
if err != nil {
return nil, ResourceNotFound.Send(ctx, fmt.Sprintf("Error getting activated versions %s", err.Error()))
Copy link
Contributor

Choose a reason for hiding this comment

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

should be versions: %s I think (same below)

var mainlineCommits MainlineCommits
activatedVersionCount := 0
for _, v := range versions {
if activatedVersionCount == *options.Limit {
Copy link
Contributor

Choose a reason for hiding this comment

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

This hasn't been answered/updated ^

} else {
if len(mainlineCommits.Versions) > 0 {
lastMainlineCommit := mainlineCommits.Versions[len(mainlineCommits.Versions)-1]
if lastMainlineCommit.RolledUpVersions != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

You can check this in the go playground, but I believe appending to a nil list does work. Otherwise if there aren't any rolled up versions yet, you're just omitting the version completely at this step.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! I added some comments and moved the logic a little bit to make it more clear what this is doing. I also updated the graphql tests to capture this scenario.

Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for these comments!

defaultSort := []task.TasksSortOrder{
{Key: task.DisplayNameKey, Order: 1},
}
opts := data.TaskFilterOptions{
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here: only need to specify Statuses, Variants, TaskNames, Sorts

if options.Statuses != nil {
statuses = options.Statuses
}
return generateBuildVariants(ctx, r.sc, *v.Id, variants, tasks, statuses)
Copy link
Contributor

Choose a reason for hiding this comment

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

You can just pass in each option directly; there's no reason we can't pass in a nil list.

var match bson.M = bson.M{}

// Allow searching by either variant name or variant display
if variant != "" {
if len(variants) > 0 {
variantsAsRegex := strings.Join(variants[:], "|")
Copy link
Contributor

Choose a reason for hiding this comment

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

^

@khelif96 khelif96 requested a review from ablack12 May 17, 2021 16:32
@ablack12 ablack12 removed the request for review from john-m-liu May 17, 2021 19:33
Copy link
Contributor

@ablack12 ablack12 left a comment

Choose a reason for hiding this comment

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

Just a couple last thoughts from me!

} else {
if len(mainlineCommits.Versions) > 0 {
lastMainlineCommit := mainlineCommits.Versions[len(mainlineCommits.Versions)-1]
if lastMainlineCommit.RolledUpVersions != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

thanks for these comments!

model/version.go Outdated
@@ -138,7 +139,7 @@ func (self *Version) SetActivated() error {
}

func (self *Version) SetNotActivated() error {
if utility.FromBoolPtr(self.Activated) {
if self.Activated != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

It's possible we'll want to use this function in the future to set something from activated to not activated (not sure why but it does seem like something that should work), so I think we only really want to return early if self.Activated is set explicitly to False already (you can use !utility.FromBoolTPtr to this, because the nil case defaults to true)


// Returns grouped build variants for a version. Will not return build variants for unactivated versions
func (r *versionResolver) BuildVariants(ctx context.Context, v *restModel.APIVersion, options *BuildVariantOptions) ([]*GroupedBuildVariant, error) {
if !utility.FromBoolPtr(v.Activated) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to consider if v.Activated is nil, or would the MainlineCommits resolver have been guaranteed to hit first?

Copy link
Contributor Author

@khelif96 khelif96 May 17, 2021

Choose a reason for hiding this comment

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

In this case it is guaranteed to have been hit first. But if we want to use this for another field that returns a version it is not guaranteed. There will be a super low likely hood of it being nil unless someone decides to view a really old version that was introduced before this field (v.Activated) was introduced

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added handling for that edge case just in case.

@khelif96 khelif96 requested a review from ablack12 May 17, 2021 20:36
model/version.go Outdated
@@ -139,7 +139,7 @@ func (self *Version) SetActivated() error {
}

func (self *Version) SetNotActivated() error {
if self.Activated != nil {
if !utility.FromBoolPtr(self.Activated) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So I suggested FromBoolTPtr here because right now, nil will default to false, which means that if self.Activated is false we'll exit early. FromBoolTPtr defaults nil to true, meaning we'll continue on to the update, which is what we want to happen (sorry about all the semantics, bool pointers are weird to reason about)

@khelif96 khelif96 requested a review from ablack12 May 18, 2021 19:19
@khelif96 khelif96 merged commit 4071b3c into main May 25, 2021
@khelif96 khelif96 deleted the EVG-13846 branch May 25, 2021 09:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants