Skip to content

Commit

Permalink
implement GET /v1/projects/:id/resources/:type/operations/recently-fa…
Browse files Browse the repository at this point in the history
…iled

Test coverage coming soon.
  • Loading branch information
majewsky committed Jun 14, 2019
1 parent 74eb5fb commit 7e175df
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 61 deletions.
3 changes: 3 additions & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ func (h *handler) BuildRouter() http.Handler {
router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/pending`).
HandlerFunc(h.GetPendingOperationsForResource)
router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/recently-failed`).
HandlerFunc(h.GetRecentlyFailedOperationsForResource)

return router
}
Expand Down
5 changes: 3 additions & 2 deletions internal/api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ func PendingOperationFromDB(dbOp db.PendingOperation, assetID string) Operation
}

//FinishedOperationFromDB converts a db.FinishedOperation into an api.Operation.
func FinishedOperationFromDB(dbOp db.FinishedOperation) Operation {
func FinishedOperationFromDB(dbOp db.FinishedOperation, assetID string) Operation {
op := Operation{
AssetID: assetID,
State: dbOp.State(),
Reason: dbOp.Reason,
OldSize: dbOp.OldSize,
Expand Down Expand Up @@ -243,7 +244,7 @@ func (h handler) GetAsset(w http.ResponseWriter, r *http.Request) {
}
finishedOps := make([]Operation, len(dbFinishedOps))
for idx, op := range dbFinishedOps {
finishedOps[idx] = FinishedOperationFromDB(op)
finishedOps[idx] = FinishedOperationFromDB(op, "")
}
asset.FinishedOperations = &finishedOps
}
Expand Down
89 changes: 89 additions & 0 deletions internal/api/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ package api

import (
"net/http"
"time"

"github.com/sapcc/castellum/internal/core"
"github.com/sapcc/castellum/internal/db"
"github.com/sapcc/go-bits/respondwith"
)
Expand All @@ -35,6 +37,7 @@ func (h handler) GetPendingOperationsForResource(w http.ResponseWriter, r *http.
return
}

//find operations
var ops []db.PendingOperation
_, err := h.DB.Select(&ops, `
SELECT o.* FROM pending_operations o
Expand All @@ -44,6 +47,8 @@ func (h handler) GetPendingOperationsForResource(w http.ResponseWriter, r *http.
if respondwith.ErrorText(w, err) {
return
}

//find asset UUIDs
assetUUIDs, err := h.getAssetUUIDMap(*dbResource)
if respondwith.ErrorText(w, err) {
return
Expand Down Expand Up @@ -80,3 +85,87 @@ func (h handler) getAssetUUIDMap(res db.Resource) (map[int64]string, error) {
}
return assetUUIDs, rows.Err()
}

func (h handler) GetRecentlyFailedOperationsForResource(w http.ResponseWriter, r *http.Request) {
projectUUID, token := h.CheckToken(w, r)
if token == nil {
return
}
dbResource := h.LoadResource(w, r, projectUUID, token, false)
if dbResource == nil {
return
}

//find failed operations
var failedOps []db.FinishedOperation
_, err := h.DB.Select(&failedOps, `
SELECT * FROM finished_operations o
JOIN assets a ON a.id = o.asset_id
WHERE a.resource_id = $1 AND o.outcome = 'failed'
`, dbResource.ID)
if respondwith.ErrorText(w, err) {
return
}

//only consider the most recent failed operation for each asset
failedOpsByAssetID := make(map[int64]db.FinishedOperation)
for _, op := range failedOps {
otherOp, exists := failedOpsByAssetID[op.AssetID]
if !exists || otherOp.FinishedAt.Before(op.FinishedAt) {
failedOpsByAssetID[op.AssetID] = op
}
}

//filter failed operations where a later operation completed without error
rows, err := h.DB.Query(`
SELECT o.asset_id, MAX(o.finished_at) FROM finished_operations o
JOIN assets a on a.id = o.asset_id
WHERE a.resource_id = $1
GROUP BY o.asset_id
`, dbResource.ID)
if respondwith.ErrorText(w, err) {
return
}
for rows.Next() {
var (
assetID int64
maxFinishedAt time.Time
)
err := rows.Scan(&assetID, &maxFinishedAt)
if respondwith.ErrorText(w, err) {
return
}
op, exists := failedOpsByAssetID[assetID]
if exists && op.FinishedAt.Before(maxFinishedAt) {
delete(failedOpsByAssetID, assetID)
}
}
if respondwith.ErrorText(w, rows.Err()) {
return
}

//check if the assets in question are still eligible for resizing
var assets []db.Asset
_, err = h.DB.Select(&assets,
`SELECT * FROM assets WHERE resource_id = $1 ORDER BY uuid`, dbResource.ID)
if respondwith.ErrorText(w, err) {
return
}
var relevantOps []Operation
for _, asset := range assets {
op, exists := failedOpsByAssetID[asset.ID]
if !exists {
continue
}
if core.GetMatchingReasons(*dbResource, asset)[op.Reason] {
relevantOps = append(relevantOps, FinishedOperationFromDB(op, asset.UUID))
}
}

//render response body
var response struct {
Operations []Operation `json:"recently_failed_operations,keepempty"`
}
response.Operations = relevantOps
respondwith.JSON(w, http.StatusOK, response)
}
5 changes: 5 additions & 0 deletions internal/api/operations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,8 @@ func TestGetPendingOperationsForResource(baseT *testing.T) {
pendingOpJSON["greenlit"] = assert.JSONObject{"at": 23, "by_user": "user1"}
req.Check(t.T, hh)
}

func TestGetRecentlyFailedOperationsForResource(baseT *testing.T) {
t := test.T{T: baseT}
t.Fatal("TODO")
}
78 changes: 78 additions & 0 deletions internal/core/logic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/******************************************************************************
*
* Copyright 2019 SAP SE
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
******************************************************************************/

package core

import "github.com/sapcc/castellum/internal/db"

//GetMatchingReasons returns a map that indicates for which resizing operations
//the given asset (within the given resource) is eligible.
func GetMatchingReasons(res db.Resource, asset db.Asset) map[db.OperationReason]bool {
result := make(map[db.OperationReason]bool)
if res.LowThresholdPercent > 0 && asset.UsagePercent <= res.LowThresholdPercent {
if canDownsize(res, asset) {
result[db.OperationReasonLow] = true
}
}
if res.HighThresholdPercent > 0 && asset.UsagePercent >= res.HighThresholdPercent {
if canUpsize(res, asset) {
result[db.OperationReasonHigh] = true
}
}
if res.CriticalThresholdPercent > 0 && asset.UsagePercent >= res.CriticalThresholdPercent {
if canUpsize(res, asset) {
result[db.OperationReasonCritical] = true
}
}
return result
}

func canDownsize(res db.Resource, asset db.Asset) bool {
if res.MinimumSize == nil {
return true
}
return GetNewSize(res, asset, false) >= *res.MinimumSize
}

func canUpsize(res db.Resource, asset db.Asset) bool {
if res.MaximumSize == nil {
return true
}
return GetNewSize(res, asset, true) <= *res.MaximumSize
}

//GetNewSize returns the target size for this asset (within this resource)
//after upsizing (for `up = true`) or downsizing (for `up = false`).
func GetNewSize(res db.Resource, asset db.Asset, up bool) uint64 {
step := (asset.Size * uint64(res.SizeStepPercent)) / 100
//a small fraction of a small value (e.g. 10% of size = 6) may round down to zero
if step == 0 {
step = 1
}

if up {
return asset.Size + step
}

//when going down, we have to take care not to end up with zero
if asset.Size < 1+step {
//^ This condition is equal to `asset.Size - step < 1`, but cannot overflow below 0.
return 1
}
return asset.Size - step
}
65 changes: 6 additions & 59 deletions internal/tasks/asset_scrape.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,17 +194,17 @@ func (c Context) maybeCreateOperation(tx *gorp.Transaction, res db.Resource, ass
CreatedAt: c.TimeNow(),
}

match := getMatchingReasons(res, asset)
match := core.GetMatchingReasons(res, asset)
switch {
case match[db.OperationReasonCritical]:
op.Reason = db.OperationReasonCritical
op.NewSize = getNewSize(res, asset, true)
op.NewSize = core.GetNewSize(res, asset, true)
case match[db.OperationReasonHigh]:
op.Reason = db.OperationReasonHigh
op.NewSize = getNewSize(res, asset, true)
op.NewSize = core.GetNewSize(res, asset, true)
case match[db.OperationReasonLow]:
op.Reason = db.OperationReasonLow
op.NewSize = getNewSize(res, asset, false)
op.NewSize = core.GetNewSize(res, asset, false)
default:
//no threshold exceeded -> do not create an operation
return nil
Expand All @@ -229,7 +229,7 @@ func (c Context) maybeCreateOperation(tx *gorp.Transaction, res db.Resource, ass

func (c Context) maybeCancelOperation(tx *gorp.Transaction, res db.Resource, asset db.Asset, op db.PendingOperation) (*db.PendingOperation, error) {
//cancel when the threshold that triggered this operation is no longer being crossed
match := getMatchingReasons(res, asset)
match := core.GetMatchingReasons(res, asset)
doCancel := !match[op.Reason]
if op.Reason == db.OperationReasonHigh && match[db.OperationReasonCritical] {
//as an exception, cancel a "High" operation when we've crossed the
Expand All @@ -253,7 +253,7 @@ func (c Context) maybeCancelOperation(tx *gorp.Transaction, res db.Resource, ass

func (c Context) maybeConfirmOperation(tx *gorp.Transaction, res db.Resource, asset db.Asset, op db.PendingOperation) (*db.PendingOperation, error) {
//can only confirm when the corresponding threshold is still being crossed
if !getMatchingReasons(res, asset)[op.Reason] {
if !core.GetMatchingReasons(res, asset)[op.Reason] {
return &op, nil
}

Expand All @@ -280,56 +280,3 @@ func (c Context) maybeConfirmOperation(tx *gorp.Transaction, res db.Resource, as
core.CountStateTransition(res, asset.UUID, previousState, op.State())
return &op, err
}

func getMatchingReasons(res db.Resource, asset db.Asset) map[db.OperationReason]bool {
result := make(map[db.OperationReason]bool)
if res.LowThresholdPercent > 0 && asset.UsagePercent <= res.LowThresholdPercent {
if canDownsize(res, asset) {
result[db.OperationReasonLow] = true
}
}
if res.HighThresholdPercent > 0 && asset.UsagePercent >= res.HighThresholdPercent {
if canUpsize(res, asset) {
result[db.OperationReasonHigh] = true
}
}
if res.CriticalThresholdPercent > 0 && asset.UsagePercent >= res.CriticalThresholdPercent {
if canUpsize(res, asset) {
result[db.OperationReasonCritical] = true
}
}
return result
}

func canDownsize(res db.Resource, asset db.Asset) bool {
if res.MinimumSize == nil {
return true
}
return getNewSize(res, asset, false) >= *res.MinimumSize
}

func canUpsize(res db.Resource, asset db.Asset) bool {
if res.MaximumSize == nil {
return true
}
return getNewSize(res, asset, true) <= *res.MaximumSize
}

func getNewSize(res db.Resource, asset db.Asset, up bool) uint64 {
step := (asset.Size * uint64(res.SizeStepPercent)) / 100
//a small fraction of a small value (e.g. 10% of size = 6) may round down to zero
if step == 0 {
step = 1
}

if up {
return asset.Size + step
}

//when going down, we have to take care not to end up with zero
if asset.Size < 1+step {
//^ This condition is equal to `asset.Size - step < 1`, but cannot overflow below 0.
return 1
}
return asset.Size - step
}

0 comments on commit 7e175df

Please sign in to comment.