-
Notifications
You must be signed in to change notification settings - Fork 2
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
CORE-2016: support scheduled rate and quota default changes. #63
Conversation
…ription rate changes
…uota-defaults endpoint
…plans/:plan-id/active-quota-defaults endpoints
…nd multiple scheduled plan quota default updates
…-id/rates` endpoint
…d endpoint and did some refactoring
#!/usr/bin/env bash | ||
QMS_DATABASE_URI=postgres://de@localhost/qms?sslmode=disable | ||
QMS_DATABASE_MIGRATE=false | ||
QMS_DATABASE_REINIT=false | ||
QMS_USERNAME_SUFFIX=iplantcollaborative.org |
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've found direnv
to be extremely useful for development testing, so I thought I'd set it up for this repository.
@@ -152,11 +153,10 @@ func (s Server) AddPlan(ctx echo.Context) error { | |||
|
|||
log.Debugf("adding plan quota default resource %s to plan %s", resourceType.Name, plan.Name) | |||
} | |||
|
|||
log.Debug("adding plan to the database") | |||
log.Debugf("translated plan: %+v", dbPlan) |
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'm on the fence about leaving this log message in place.
@@ -133,11 +125,20 @@ func (s Server) AddPlan(ctx echo.Context) error { | |||
} | |||
|
|||
log = log.WithFields(logrus.Fields{"plan": plan.Name}) | |||
log.Debug("adding plan to the database") | |||
log.Debugf("adding a new plan to the database: %+v", plan) |
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'm on the fence about logging all of the plan details in this message.
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 that for this and the other log messages, as long as they're on Debug level anyway, I'm not that worried. They won't be running with that in prod anyway.
Preload("PlanQuotaDefaults", func(db *gorm.DB) *gorm.DB { | ||
return db. | ||
Joins("INNER JOIN resource_types ON plan_quota_defaults.resource_type_id = resource_types.id"). | ||
Order("plan_quota_defaults.effective_date asc, resource_types.name asc") | ||
}). |
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.
The purpose of this is just to set the sort order. I was a little afraid that the join would cause problems with field value resolution, but it seems to be working.
Select("DISTINCT ON (resource_type_id) resource_type_id", "id", "plan_id", "quota_value", "effective_date"). | ||
Where("effective_date <= CURRENT_TIMESTAMP AND plan_id = ?", planID). |
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 appears to do what I want it to do, but SQL sanity checks are always a good thing. 😄
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 feel like I'm not quite following what this does. I'd expect from the name for it to return only one default for each resource type in the active plan, but it seems like if there were to be two defaults with different effective dates but both in the past, it'd return both of them. But there's possibly something clever I'm missing, heh.
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.
Fortunately, it's only grabbing the most recent one that has an effective date before the current date, which is what I want it to do. If I'm understanding it correctly, it works because the DISTINCT ON
forces it to grab only the first matching row satisfying the conditions for each resource type. The sort order then ensures that the most recent quota default for each resource type is the one that gets chosen.
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.
Ah, yes, I see. I confirmed with the PostgreSQL docs and it does explicitly say there that it's the first row and which row is first should be controlled by ORDER BY, so seems perfect.
LGTM now that I wrapped my head around it 😁
@@ -6,6 +6,6 @@ BEGIN; | |||
|
|||
SET search_path = public, pg_catalog; | |||
|
|||
ALTER TABLE resource_types DROP IF EXISTS consumable CASCADE; |
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'd forgotten to add the existence check here before.
@@ -6,7 +6,7 @@ BEGIN; | |||
|
|||
SET search_path = public, pg_catalog; | |||
|
|||
ALTER TABLE resource_types ADD IF NOT EXISTS consumable boolean DEFAULT FALSE; |
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'd forgotten to add the existence check here before as well.
// Lists all of the active user plans. | ||
users.GET("/all_active_users", s.GetAllActiveSubscriptions) |
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 endpoint has never been used as far as I'm aware.
// GET /:username/resources/overages returns summaries of any usages that exceed the quota for the corresponding resource. | ||
users.GET("/:username/resources/overages", s.GetUserOverages) | ||
|
||
// GET /:username/resources/:resource-name/overage returns whether the usage exceeds the quota for the resource. | ||
users.GET("/:username/resources/:resource-name/in-overage", s.InOverage) |
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.
The overage endpoints are now implemented in the subscriptions
service.
// Adds or updates the quota defaults of a plan. | ||
plans.POST("/quota-defaults", s.AddPlanQuotaDefault) |
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.
The old endpoint to add plan quota defaults was a little clunky, so I reimplemented 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 don't feel like I totally grok how gorm works, but other than a few comments on style stuff, this appears basically fine to me. I'm not really following the spot you asked for SQL feedback, so I might be able to review that more carefully with a bit more understanding.
internal/model/plan.go
Outdated
} | ||
|
||
// GetDefaultQuotaValue returns the default quota value associated with the resource type with the given name. | ||
// GetDefaultQuotaValue returns the default quota value associated with the resource type with the given name. This | ||
// funciton assumes that the plan quota defaults ar sorted in ascending order by effective date. | ||
func (p *Plan) GetDefaultQuotaValue(resourcetypeName string) float64 { |
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'm not sure if this is helpful or not, but it'd at least cut down lines of code if this function was implemented using the newly-added GetDefaultQuotaValues
instead. Really the only thing this function is doing differently is a conditional on the resource type name rather than using that name as a key, and I believe it'll look at the same collection of quota defaults since they both just range over it and break when they pass time.Now()
.
(the comment also has a couple typos, which doesn't really matter at all)
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.
Definitely helpful. Thanks for the suggestion!
@@ -133,11 +125,20 @@ func (s Server) AddPlan(ctx echo.Context) error { | |||
} | |||
|
|||
log = log.WithFields(logrus.Fields{"plan": plan.Name}) | |||
log.Debug("adding plan to the database") | |||
log.Debugf("adding a new plan to the database: %+v", plan) |
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 that for this and the other log messages, as long as they're on Debug level anyway, I'm not that worried. They won't be running with that in prod anyway.
Select("DISTINCT ON (resource_type_id) resource_type_id", "id", "plan_id", "quota_value", "effective_date"). | ||
Where("effective_date <= CURRENT_TIMESTAMP AND plan_id = ?", planID). |
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 feel like I'm not quite following what this does. I'd expect from the name for it to return only one default for each resource type in the active plan, but it seems like if there were to be two defaults with different effective dates but both in the past, it'd return both of them. But there's possibly something clever I'm missing, heh.
currentIndex := 0 | ||
for _, quotaDefault := range pqds { | ||
quotaValue := quotaDefault.QuotaValue | ||
if quotaDefault.ResourceType.Consumable { | ||
quotaValue *= float64(periods) | ||
} | ||
result[i] = model.Quota{ | ||
result[currentIndex] = model.Quota{ | ||
Quota: quotaValue, | ||
ResourceTypeID: quotaDefault.ResourceTypeID, | ||
} | ||
currentIndex++ |
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'm missing if there's a benefit to managing the currentIndex
outside of the loop, since it appears to unconditionally increment it at the end of the loop iteration. It seems like this could be done more cleanly with for currentIndex, quotaDefault := range pqds
(more similarly to the old code)
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.
The reason in this case is because the plan quota defaults are now in a map to ensure that we only have one plan quota default for each resource type.
I considered making another call to the database to get the active plan quota defaults, which would turn the data structure back into an array, but since we already have everything we need in memory, finding the correct plan quota defaults in code seemed to be the better option just to avoid another round trip to the database. I'm still on the fence about this decision, though. If enough plan quota defaults are added then the time saved by avoiding a round trip to the database might eventually become negligible (or be eliminated entirely).
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.
Ah! That makes sense. I had failed to notice it was a map, and thus wouldn't have indices to use.
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.
To be honest, I'd forgotten about it too and noticed it when I started to update the code. 😄
…uotaValues for added clarity
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 the review!
Select("DISTINCT ON (resource_type_id) resource_type_id", "id", "plan_id", "quota_value", "effective_date"). | ||
Where("effective_date <= CURRENT_TIMESTAMP AND plan_id = ?", planID). |
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.
Fortunately, it's only grabbing the most recent one that has an effective date before the current date, which is what I want it to do. If I'm understanding it correctly, it works because the DISTINCT ON
forces it to grab only the first matching row satisfying the conditions for each resource type. The sort order then ensures that the most recent quota default for each resource type is the one that gets chosen.
currentIndex := 0 | ||
for _, quotaDefault := range pqds { | ||
quotaValue := quotaDefault.QuotaValue | ||
if quotaDefault.ResourceType.Consumable { | ||
quotaValue *= float64(periods) | ||
} | ||
result[i] = model.Quota{ | ||
result[currentIndex] = model.Quota{ | ||
Quota: quotaValue, | ||
ResourceTypeID: quotaDefault.ResourceTypeID, | ||
} | ||
currentIndex++ |
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.
The reason in this case is because the plan quota defaults are now in a map to ensure that we only have one plan quota default for each resource type.
I considered making another call to the database to get the active plan quota defaults, which would turn the data structure back into an array, but since we already have everything we need in memory, finding the correct plan quota defaults in code seemed to be the better option just to avoid another round trip to the database. I'm still on the fence about this decision, though. If enough plan quota defaults are added then the time saved by avoiding a round trip to the database might eventually become negligible (or be eliminated entirely).
Thanks for the review! |
This change allows both rates and default quota values to be updated for a plan. Plan rates are a new feature. The rate associated with the plan is associated with the subscription plan. Multiple rates can be associated with the plan, and the currently active plan rate is always the one with the most recent effective date that is on or before the current date. Plan quota defaults are implemented in the same way, but there's a different active plan quota default for each resource type.