summaryallocation.go 56 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846
  1. package kubecost
  2. import (
  3. "errors"
  4. "fmt"
  5. "math"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/opencost/opencost/pkg/filter21/ast"
  10. "github.com/opencost/opencost/pkg/filter21/matcher"
  11. "github.com/opencost/opencost/pkg/log"
  12. "github.com/opencost/opencost/pkg/util/timeutil"
  13. )
  14. // SummaryAllocation summarizes an Allocation, keeping only fields necessary
  15. // for providing a high-level view of identifying the Allocation over a period
  16. // of time (Start, End) over which it ran, and inspecting the associated per-
  17. // resource costs (subtotaled with adjustments), total cost, and efficiency.
  18. //
  19. // SummaryAllocation does not have a concept of Window (i.e. the time period
  20. // within which it is defined, as opposed to the Start and End times). That
  21. // context must be provided by a SummaryAllocationSet.
  22. type SummaryAllocation struct {
  23. Name string `json:"name"`
  24. Properties *AllocationProperties `json:"-"`
  25. Start time.Time `json:"start"`
  26. End time.Time `json:"end"`
  27. CPUCoreRequestAverage float64 `json:"cpuCoreRequestAverage"`
  28. CPUCoreUsageAverage float64 `json:"cpuCoreUsageAverage"`
  29. CPUCost float64 `json:"cpuCost"`
  30. GPUCost float64 `json:"gpuCost"`
  31. NetworkCost float64 `json:"networkCost"`
  32. LoadBalancerCost float64 `json:"loadBalancerCost"`
  33. PVCost float64 `json:"pvCost"`
  34. RAMBytesRequestAverage float64 `json:"ramByteRequestAverage"`
  35. RAMBytesUsageAverage float64 `json:"ramByteUsageAverage"`
  36. RAMCost float64 `json:"ramCost"`
  37. SharedCost float64 `json:"sharedCost"`
  38. ExternalCost float64 `json:"externalCost"`
  39. Share bool `json:"-"`
  40. UnmountedPVCost float64 `json:"-"`
  41. }
  42. // NewSummaryAllocation converts an Allocation to a SummaryAllocation by
  43. // dropping unnecessary fields and consolidating others (e.g. adjustments).
  44. // Reconciliation happens here because that process is synonymous with the
  45. // consolidation of adjustment fields.
  46. func NewSummaryAllocation(alloc *Allocation, reconcile, reconcileNetwork bool) *SummaryAllocation {
  47. if alloc == nil {
  48. return nil
  49. }
  50. sa := &SummaryAllocation{
  51. Name: alloc.Name,
  52. Properties: alloc.Properties,
  53. Start: alloc.Start,
  54. End: alloc.End,
  55. CPUCoreRequestAverage: alloc.CPUCoreRequestAverage,
  56. CPUCoreUsageAverage: alloc.CPUCoreUsageAverage,
  57. CPUCost: alloc.CPUCost + alloc.CPUCostAdjustment,
  58. GPUCost: alloc.GPUCost + alloc.GPUCostAdjustment,
  59. NetworkCost: alloc.NetworkCost + alloc.NetworkCostAdjustment,
  60. LoadBalancerCost: alloc.LoadBalancerCost + alloc.LoadBalancerCostAdjustment,
  61. PVCost: alloc.PVCost() + alloc.PVCostAdjustment,
  62. RAMBytesRequestAverage: alloc.RAMBytesRequestAverage,
  63. RAMBytesUsageAverage: alloc.RAMBytesUsageAverage,
  64. RAMCost: alloc.RAMCost + alloc.RAMCostAdjustment,
  65. SharedCost: alloc.SharedCost,
  66. ExternalCost: alloc.ExternalCost,
  67. UnmountedPVCost: alloc.UnmountedPVCost,
  68. }
  69. // Revert adjustments if reconciliation is off. If only network
  70. // reconciliation is off, only revert network adjustment.
  71. if !reconcile {
  72. sa.CPUCost -= alloc.CPUCostAdjustment
  73. sa.GPUCost -= alloc.GPUCostAdjustment
  74. sa.NetworkCost -= alloc.NetworkCostAdjustment
  75. sa.LoadBalancerCost -= alloc.LoadBalancerCostAdjustment
  76. sa.PVCost -= alloc.PVCostAdjustment
  77. sa.RAMCost -= alloc.RAMCostAdjustment
  78. } else if !reconcileNetwork {
  79. sa.NetworkCost -= alloc.NetworkCostAdjustment
  80. }
  81. // If the allocation is unmounted, set UnmountedPVCost to the full PVCost.
  82. if sa.IsUnmounted() {
  83. sa.UnmountedPVCost = sa.PVCost
  84. }
  85. return sa
  86. }
  87. // Add sums two SummaryAllocations, adding the given SummaryAllocation to the
  88. // receiving one, thus mutating the receiver. For performance reasons, it
  89. // simply drops Properties, so a SummaryAllocation can only be Added once.
  90. func (sa *SummaryAllocation) Add(that *SummaryAllocation) error {
  91. if sa == nil || that == nil {
  92. return errors.New("cannot Add a nil SummaryAllocation")
  93. }
  94. // Once Added, a SummaryAllocation has no Properties. This saves us from
  95. // having to compute the intersection of two sets of Properties, which is
  96. // expensive.
  97. sa.Properties = nil
  98. sa.sanitizeNaN()
  99. that.sanitizeNaN()
  100. // Sum non-cumulative fields by turning them into cumulative, adding them,
  101. // and then converting them back into averages after minutes have been
  102. // combined (just below).
  103. cpuReqCoreMins := sa.CPUCoreRequestAverage * sa.Minutes()
  104. cpuReqCoreMins += that.CPUCoreRequestAverage * that.Minutes()
  105. cpuUseCoreMins := sa.CPUCoreUsageAverage * sa.Minutes()
  106. cpuUseCoreMins += that.CPUCoreUsageAverage * that.Minutes()
  107. ramReqByteMins := sa.RAMBytesRequestAverage * sa.Minutes()
  108. ramReqByteMins += that.RAMBytesRequestAverage * that.Minutes()
  109. ramUseByteMins := sa.RAMBytesUsageAverage * sa.Minutes()
  110. ramUseByteMins += that.RAMBytesUsageAverage * that.Minutes()
  111. // Expand Start and End to be the "max" of among the given Allocations
  112. if that.Start.Before(sa.Start) {
  113. sa.Start = that.Start
  114. }
  115. if that.End.After(sa.End) {
  116. sa.End = that.End
  117. }
  118. // Convert cumulative request and usage back into rates
  119. if sa.Minutes() > 0 {
  120. sa.CPUCoreRequestAverage = cpuReqCoreMins / sa.Minutes()
  121. sa.CPUCoreUsageAverage = cpuUseCoreMins / sa.Minutes()
  122. sa.RAMBytesRequestAverage = ramReqByteMins / sa.Minutes()
  123. sa.RAMBytesUsageAverage = ramUseByteMins / sa.Minutes()
  124. } else {
  125. sa.CPUCoreRequestAverage = 0.0
  126. sa.CPUCoreUsageAverage = 0.0
  127. sa.RAMBytesRequestAverage = 0.0
  128. sa.RAMBytesUsageAverage = 0.0
  129. }
  130. // Sum all cumulative cost fields
  131. sa.CPUCost += that.CPUCost
  132. sa.ExternalCost += that.ExternalCost
  133. sa.GPUCost += that.GPUCost
  134. sa.LoadBalancerCost += that.LoadBalancerCost
  135. sa.NetworkCost += that.NetworkCost
  136. sa.PVCost += that.PVCost
  137. sa.RAMCost += that.RAMCost
  138. sa.SharedCost += that.SharedCost
  139. return nil
  140. }
  141. // Clone copies the SummaryAllocation and returns the copy
  142. func (sa *SummaryAllocation) Clone() *SummaryAllocation {
  143. return &SummaryAllocation{
  144. Name: sa.Name,
  145. Properties: sa.Properties.Clone(),
  146. Start: sa.Start,
  147. End: sa.End,
  148. CPUCoreRequestAverage: sa.CPUCoreRequestAverage,
  149. CPUCoreUsageAverage: sa.CPUCoreUsageAverage,
  150. CPUCost: sa.CPUCost,
  151. GPUCost: sa.GPUCost,
  152. NetworkCost: sa.NetworkCost,
  153. LoadBalancerCost: sa.LoadBalancerCost,
  154. PVCost: sa.PVCost,
  155. RAMBytesRequestAverage: sa.RAMBytesRequestAverage,
  156. RAMBytesUsageAverage: sa.RAMBytesUsageAverage,
  157. RAMCost: sa.RAMCost,
  158. SharedCost: sa.SharedCost,
  159. ExternalCost: sa.ExternalCost,
  160. }
  161. }
  162. // CPUEfficiency is the ratio of usage to request. If there is no request and
  163. // no usage or cost, then efficiency is zero. If there is no request, but there
  164. // is usage or cost, then efficiency is 100%.
  165. func (sa *SummaryAllocation) CPUEfficiency() float64 {
  166. if sa == nil || sa.IsIdle() {
  167. return 0.0
  168. }
  169. sa.sanitizeNaN()
  170. if sa.CPUCoreRequestAverage > 0 {
  171. return sa.CPUCoreUsageAverage / sa.CPUCoreRequestAverage
  172. }
  173. if sa.CPUCoreUsageAverage == 0.0 || sa.CPUCost == 0.0 {
  174. return 0.0
  175. }
  176. return 1.0
  177. }
  178. func (sa *SummaryAllocation) Equal(that *SummaryAllocation) bool {
  179. if sa == nil || that == nil {
  180. return false
  181. }
  182. if sa.Name != that.Name {
  183. return false
  184. }
  185. if sa.Start != that.Start {
  186. return false
  187. }
  188. if sa.End != that.End {
  189. return false
  190. }
  191. if sa.CPUCoreRequestAverage != that.CPUCoreRequestAverage {
  192. return false
  193. }
  194. if sa.CPUCoreUsageAverage != that.CPUCoreUsageAverage {
  195. return false
  196. }
  197. if sa.CPUCost != that.CPUCost {
  198. return false
  199. }
  200. if sa.GPUCost != that.GPUCost {
  201. return false
  202. }
  203. if sa.NetworkCost != that.NetworkCost {
  204. return false
  205. }
  206. if sa.LoadBalancerCost != that.LoadBalancerCost {
  207. return false
  208. }
  209. if sa.PVCost != that.PVCost {
  210. return false
  211. }
  212. if sa.RAMBytesRequestAverage != that.RAMBytesRequestAverage {
  213. return false
  214. }
  215. if sa.RAMBytesUsageAverage != that.RAMBytesUsageAverage {
  216. return false
  217. }
  218. if sa.RAMCost != that.RAMCost {
  219. return false
  220. }
  221. if sa.SharedCost != that.SharedCost {
  222. return false
  223. }
  224. if sa.ExternalCost != that.ExternalCost {
  225. return false
  226. }
  227. return true
  228. }
  229. func (sa *SummaryAllocation) generateKey(aggregateBy []string, labelConfig *LabelConfig) string {
  230. if sa == nil {
  231. return ""
  232. }
  233. return sa.Properties.GenerateKey(aggregateBy, labelConfig)
  234. }
  235. // IsExternal is true if the given SummaryAllocation represents external costs.
  236. func (sa *SummaryAllocation) IsExternal() bool {
  237. if sa == nil {
  238. return false
  239. }
  240. return strings.Contains(sa.Name, ExternalSuffix)
  241. }
  242. // IsIdle is true if the given SummaryAllocation represents idle costs.
  243. func (sa *SummaryAllocation) IsIdle() bool {
  244. if sa == nil {
  245. return false
  246. }
  247. return strings.Contains(sa.Name, IdleSuffix)
  248. }
  249. // IsUnallocated is true if the given SummaryAllocation represents unallocated
  250. // costs.
  251. func (sa *SummaryAllocation) IsUnallocated() bool {
  252. if sa == nil {
  253. return false
  254. }
  255. return strings.Contains(sa.Name, UnallocatedSuffix)
  256. }
  257. // IsUnmounted is true if the given SummaryAllocation represents unmounted
  258. // volume costs.
  259. func (sa *SummaryAllocation) IsUnmounted() bool {
  260. if sa == nil {
  261. return false
  262. }
  263. return strings.Contains(sa.Name, UnmountedSuffix)
  264. }
  265. // Minutes returns the number of minutes the SummaryAllocation represents, as
  266. // defined by the difference between the end and start times.
  267. func (sa *SummaryAllocation) Minutes() float64 {
  268. if sa == nil {
  269. return 0.0
  270. }
  271. return sa.End.Sub(sa.Start).Minutes()
  272. }
  273. // RAMEfficiency is the ratio of usage to request. If there is no request and
  274. // no usage or cost, then efficiency is zero. If there is no request, but there
  275. // is usage or cost, then efficiency is 100%.
  276. func (sa *SummaryAllocation) RAMEfficiency() float64 {
  277. if sa == nil || sa.IsIdle() {
  278. return 0.0
  279. }
  280. sa.sanitizeNaN()
  281. if sa.RAMBytesRequestAverage > 0 {
  282. return sa.RAMBytesUsageAverage / sa.RAMBytesRequestAverage
  283. }
  284. if sa.RAMBytesUsageAverage == 0.0 || sa.RAMCost == 0.0 {
  285. return 0.0
  286. }
  287. return 1.0
  288. }
  289. // TotalCost is the total cost of the SummaryAllocation
  290. func (sa *SummaryAllocation) TotalCost() float64 {
  291. if sa == nil {
  292. return 0.0
  293. }
  294. sa.sanitizeNaN()
  295. return sa.CPUCost + sa.GPUCost + sa.RAMCost + sa.PVCost + sa.NetworkCost + sa.LoadBalancerCost + sa.SharedCost + sa.ExternalCost
  296. }
  297. // TotalEfficiency is the cost-weighted average of CPU and RAM efficiency. If
  298. // there is no cost at all, then efficiency is zero.
  299. func (sa *SummaryAllocation) TotalEfficiency() float64 {
  300. if sa == nil || sa.IsIdle() {
  301. return 0.0
  302. }
  303. sa.sanitizeNaN()
  304. if sa.RAMCost+sa.CPUCost > 0 {
  305. ramCostEff := sa.RAMEfficiency() * sa.RAMCost
  306. cpuCostEff := sa.CPUEfficiency() * sa.CPUCost
  307. return (ramCostEff + cpuCostEff) / (sa.CPUCost + sa.RAMCost)
  308. }
  309. return 0.0
  310. }
  311. func (sa *SummaryAllocation) sanitizeNaN() {
  312. if math.IsNaN(sa.CPUCoreRequestAverage) {
  313. sa.CPUCoreRequestAverage = 0
  314. }
  315. if math.IsNaN(sa.CPUCoreUsageAverage) {
  316. sa.CPUCoreUsageAverage = 0
  317. }
  318. if math.IsNaN(sa.CPUCost) {
  319. sa.CPUCost = 0
  320. }
  321. if math.IsNaN(sa.GPUCost) {
  322. sa.GPUCost = 0
  323. }
  324. if math.IsNaN(sa.NetworkCost) {
  325. sa.NetworkCost = 0
  326. }
  327. if math.IsNaN(sa.LoadBalancerCost) {
  328. sa.LoadBalancerCost = 0
  329. }
  330. if math.IsNaN(sa.PVCost) {
  331. sa.PVCost = 0
  332. }
  333. if math.IsNaN(sa.RAMBytesRequestAverage) {
  334. sa.RAMBytesRequestAverage = 0
  335. }
  336. if math.IsNaN(sa.RAMBytesUsageAverage) {
  337. sa.RAMBytesUsageAverage = 0
  338. }
  339. if math.IsNaN(sa.RAMCost) {
  340. sa.RAMCost = 0
  341. }
  342. if math.IsNaN(sa.SharedCost) {
  343. sa.SharedCost = 0
  344. }
  345. if math.IsNaN(sa.ExternalCost) {
  346. sa.ExternalCost = 0
  347. }
  348. }
  349. // SummaryAllocationSet stores a set of SummaryAllocations, each with a unique
  350. // name, that share a window. An AllocationSet is mutable, so treat it like a
  351. // threadsafe map.
  352. type SummaryAllocationSet struct {
  353. sync.RWMutex
  354. externalKeys map[string]bool
  355. idleKeys map[string]bool
  356. SummaryAllocations map[string]*SummaryAllocation `json:"allocations"`
  357. Window Window `json:"window"`
  358. }
  359. // NewSummaryAllocationSet converts an AllocationSet to a SummaryAllocationSet.
  360. // Filter functions, keep functions, and reconciliation parameters are
  361. // required for unfortunate reasons to do with performance and legacy order-of-
  362. // operations details, as well as the fact that reconciliation has been
  363. // pushed down to the conversion step between Allocation and SummaryAllocation.
  364. //
  365. // This filter is an AllocationMatcher, not an AST, because at this point we
  366. // already have the data and want to make sure that the filter has already
  367. // gone through a compile step to deal with things like aliases.
  368. func NewSummaryAllocationSet(as *AllocationSet, filter, keep AllocationMatcher, reconcile, reconcileNetwork bool) *SummaryAllocationSet {
  369. if as == nil {
  370. return nil
  371. }
  372. // If we can know the exact size of the map, use it. If filters or sharing
  373. // functions are present, we can't know the size, so we make a default map.
  374. var sasMap map[string]*SummaryAllocation
  375. if filter == nil {
  376. // No filters, so make the map of summary allocations exactly the size
  377. // of the origin allocation set.
  378. sasMap = make(map[string]*SummaryAllocation, len(as.Allocations))
  379. } else {
  380. // There are filters, so start with a standard map
  381. sasMap = make(map[string]*SummaryAllocation)
  382. }
  383. sas := &SummaryAllocationSet{
  384. SummaryAllocations: sasMap,
  385. Window: as.Window.Clone(),
  386. }
  387. for _, alloc := range as.Allocations {
  388. // First, detect if the allocation should be kept. If so, mark it as
  389. // such, insert it, and continue.
  390. if keep != nil && keep.Matches(alloc) {
  391. sa := NewSummaryAllocation(alloc, reconcile, reconcileNetwork)
  392. sa.Share = true
  393. sas.Insert(sa)
  394. continue
  395. }
  396. // If the allocation does not pass any of the given filter functions,
  397. // do not insert it into the set.
  398. if filter != nil && !filter.Matches(alloc) {
  399. continue
  400. }
  401. err := sas.Insert(NewSummaryAllocation(alloc, reconcile, reconcileNetwork))
  402. if err != nil {
  403. log.Errorf("SummaryAllocation: error inserting summary of %s", alloc.Name)
  404. }
  405. }
  406. for key := range as.ExternalKeys {
  407. sas.externalKeys[key] = true
  408. }
  409. for key := range as.IdleKeys {
  410. sas.idleKeys[key] = true
  411. }
  412. return sas
  413. }
  414. // Clone creates a deep copy of the SummaryAllocationSet
  415. func (sas *SummaryAllocationSet) Clone() *SummaryAllocationSet {
  416. sas.RLock()
  417. defer sas.RUnlock()
  418. externalKeys := make(map[string]bool, len(sas.externalKeys))
  419. for k, v := range sas.externalKeys {
  420. externalKeys[k] = v
  421. }
  422. idleKeys := make(map[string]bool, len(sas.idleKeys))
  423. for k, v := range sas.idleKeys {
  424. idleKeys[k] = v
  425. }
  426. summaryAllocations := make(map[string]*SummaryAllocation, len(sas.SummaryAllocations))
  427. for k, v := range sas.SummaryAllocations {
  428. summaryAllocations[k] = v.Clone()
  429. }
  430. return &SummaryAllocationSet{
  431. externalKeys: externalKeys,
  432. idleKeys: idleKeys,
  433. SummaryAllocations: summaryAllocations,
  434. Window: sas.Window.Clone(),
  435. }
  436. }
  437. // Add sums two SummaryAllocationSets, which Adds all SummaryAllocations in the
  438. // given SummaryAllocationSet to their counterparts in the receiving set. Add
  439. // also expands the Window to include both constituent Windows, in the case
  440. // that Add is being used from accumulating (as opposed to aggregating). For
  441. // performance reasons, the function may return either a new set, or an
  442. // unmodified original, so it should not be assumed that the original sets are
  443. // safeuly usable after calling Add.
  444. func (sas *SummaryAllocationSet) Add(that *SummaryAllocationSet) (*SummaryAllocationSet, error) {
  445. if sas == nil || len(sas.SummaryAllocations) == 0 {
  446. return that, nil
  447. }
  448. if that == nil || len(that.SummaryAllocations) == 0 {
  449. return sas, nil
  450. }
  451. if sas.Window.IsOpen() {
  452. return nil, errors.New("cannot add a SummaryAllocationSet with an open window")
  453. }
  454. // Set start, end to min(start), max(end)
  455. start := *sas.Window.Start()
  456. end := *sas.Window.End()
  457. if that.Window.Start().Before(start) {
  458. start = *that.Window.Start()
  459. }
  460. if that.Window.End().After(end) {
  461. end = *that.Window.End()
  462. }
  463. acc := &SummaryAllocationSet{
  464. SummaryAllocations: make(map[string]*SummaryAllocation, len(sas.SummaryAllocations)),
  465. Window: NewClosedWindow(start, end),
  466. }
  467. sas.RLock()
  468. defer sas.RUnlock()
  469. that.RLock()
  470. defer that.RUnlock()
  471. for _, alloc := range sas.SummaryAllocations {
  472. err := acc.Insert(alloc)
  473. if err != nil {
  474. return nil, err
  475. }
  476. }
  477. for _, alloc := range that.SummaryAllocations {
  478. err := acc.Insert(alloc)
  479. if err != nil {
  480. return nil, err
  481. }
  482. }
  483. return acc, nil
  484. }
  485. func (sas *SummaryAllocationSet) GetUnmountedPVCost() float64 {
  486. upvc := 0.0
  487. for _, sa := range sas.SummaryAllocations {
  488. upvc += sa.UnmountedPVCost
  489. }
  490. return upvc
  491. }
  492. // AggregateBy aggregates the Allocations in the given AllocationSet by the given
  493. // AllocationProperty. This will only be legal if the AllocationSet is divisible by the
  494. // given AllocationProperty; e.g. Containers can be divided by Namespace, but not vice-a-versa.
  495. func (sas *SummaryAllocationSet) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  496. if sas == nil || len(sas.SummaryAllocations) == 0 {
  497. return nil
  498. }
  499. if sas.Window.IsOpen() {
  500. return errors.New("cannot aggregate a SummaryAllocationSet with an open window")
  501. }
  502. if options == nil {
  503. options = &AllocationAggregationOptions{}
  504. }
  505. if options.LabelConfig == nil {
  506. options.LabelConfig = NewLabelConfig()
  507. }
  508. var filter AllocationMatcher
  509. if options.Filter == nil {
  510. filter = &matcher.AllPass[*Allocation]{}
  511. } else {
  512. compiler := NewAllocationMatchCompiler(options.LabelConfig)
  513. var err error
  514. filter, err = compiler.Compile(options.Filter)
  515. if err != nil {
  516. return fmt.Errorf("compiling filter '%s': %w", ast.ToPreOrderShortString(options.Filter), err)
  517. }
  518. }
  519. if filter == nil {
  520. return fmt.Errorf("unexpected nil filter")
  521. }
  522. // Check if we have any work to do; if not, then early return. If
  523. // aggregateBy is nil, we don't aggregate anything. On the other hand,
  524. // an empty slice implies that we should aggregate everything. (See
  525. // generateKey for why that makes sense.)
  526. shouldAggregate := aggregateBy != nil
  527. shouldKeep := len(options.SharedHourlyCosts) > 0 || options.Share != nil
  528. if !shouldAggregate && !shouldKeep {
  529. return nil
  530. }
  531. // The order of operations for aggregating a SummaryAllotionSet is as
  532. // follows:
  533. //
  534. // 1. Partition external, idle, and shared allocations into separate sets.
  535. // Also, create the resultSet into which the results will be aggregated.
  536. //
  537. // 2. Record resource totals for shared costs and unmounted volumes so
  538. // that we can account for them in computing idle coefficients.
  539. //
  540. // 3. Retrieve pre-computed allocation resource totals, which will be used
  541. // to compute idle sharing coefficients.
  542. //
  543. // 4. Convert shared hourly cost into a cumulative allocation to share,
  544. // and insert it into the share set.
  545. //
  546. // 5. Compute sharing coefficients per-aggregation, if sharing resources.
  547. //
  548. // 6. Distribute idle allocations according to the idle coefficients.
  549. //
  550. // 7. Record allocation resource totals (after filtration) if filters have
  551. // been applied. (Used for filtering proportional amount of idle.)
  552. //
  553. // 8. Generate aggregation key and insert allocation into the output set
  554. //
  555. // 9. If idle is shared and resources are shared, it's probable that some
  556. // amount of idle cost will be shared with a shared resource.
  557. // Distribute that idle cost, if it exists, among the respective shared
  558. // allocations before sharing them with the aggregated allocations.
  559. //
  560. // 10. Apply idle filtration, which "filters" the idle cost, or scales it
  561. // by the proportion of allocation resources remaining after filters
  562. // have been applied.
  563. //
  564. // 11. Distribute shared resources according to sharing coefficients.
  565. //
  566. // 12. Insert external allocations into the result set.
  567. //
  568. // 13. Insert any undistributed idle, in the case that idle
  569. // coefficients end up being zero and some idle is not shared.
  570. //
  571. // 14. Combine all idle allocations into a single idle allocation, unless
  572. // the option to keep idle split by cluster or node is enabled.
  573. // 1. Partition external, idle, and shared allocations into separate sets.
  574. // Also, create the resultSet into which the results will be aggregated.
  575. // resultSet will collect the aggregated allocations
  576. resultSet := &SummaryAllocationSet{
  577. Window: sas.Window.Clone(),
  578. }
  579. // externalSet will collect external allocations
  580. externalSet := &SummaryAllocationSet{
  581. Window: sas.Window.Clone(),
  582. }
  583. // idleSet will be shared among resultSet after initial aggregation
  584. // is complete
  585. idleSet := &SummaryAllocationSet{
  586. Window: sas.Window.Clone(),
  587. }
  588. // shareSet will be shared among resultSet after initial aggregation
  589. // is complete
  590. shareSet := &SummaryAllocationSet{
  591. Window: sas.Window.Clone(),
  592. }
  593. sas.Lock()
  594. defer sas.Unlock()
  595. // 2. Record resource totals for shared costs, aggregating by cluster or by
  596. // node (depending on if idle is partitioned by cluster or node) so that we
  597. // can account for them in computing idle coefficients. Do the same for
  598. // unmounted volume costs, which only require a total cost.
  599. sharedResourceTotals := map[string]*AllocationTotals{}
  600. totalUnmountedCost := 0.0
  601. // 1 & 2. Identify set membership and aggregate aforementioned totals.
  602. for _, sa := range sas.SummaryAllocations {
  603. sa.sanitizeNaN()
  604. if sa.Share {
  605. var key string
  606. if options.IdleByNode {
  607. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  608. } else {
  609. key = sa.Properties.Cluster
  610. }
  611. if _, ok := sharedResourceTotals[key]; !ok {
  612. sharedResourceTotals[key] = &AllocationTotals{}
  613. }
  614. sharedResourceTotals[key].CPUCost += sa.CPUCost
  615. sharedResourceTotals[key].GPUCost += sa.GPUCost
  616. sharedResourceTotals[key].LoadBalancerCost += sa.LoadBalancerCost
  617. sharedResourceTotals[key].NetworkCost += sa.NetworkCost
  618. sharedResourceTotals[key].PersistentVolumeCost += sa.PVCost
  619. sharedResourceTotals[key].RAMCost += sa.RAMCost
  620. sharedResourceTotals[key].UnmountedPVCost += sa.UnmountedPVCost
  621. shareSet.Insert(sa)
  622. delete(sas.SummaryAllocations, sa.Name)
  623. continue
  624. }
  625. // External allocations get aggregated post-hoc (see step 6) and do
  626. // not necessarily contain complete sets of properties, so they are
  627. // moved to a separate AllocationSet.
  628. if sa.IsExternal() {
  629. delete(sas.externalKeys, sa.Name)
  630. delete(sas.SummaryAllocations, sa.Name)
  631. externalSet.Insert(sa)
  632. continue
  633. }
  634. // Idle allocations should be separated into idleSet if they are to be
  635. // shared later on. If they are not to be shared, then add them to the
  636. // resultSet like any other allocation.
  637. if sa.IsIdle() {
  638. delete(sas.idleKeys, sa.Name)
  639. delete(sas.SummaryAllocations, sa.Name)
  640. if options.ShareIdle == ShareEven || options.ShareIdle == ShareWeighted {
  641. idleSet.Insert(sa)
  642. } else {
  643. resultSet.Insert(sa)
  644. }
  645. continue
  646. }
  647. // Track total unmounted cost because it must be taken out of total
  648. // allocated costs for sharing coefficients.
  649. if sa.IsUnmounted() {
  650. totalUnmountedCost += sa.TotalCost()
  651. }
  652. }
  653. // It's possible that no more un-shared, non-idle, non-external allocations
  654. // remain at this point. This always results in an emptySet, so return early.
  655. if len(sas.SummaryAllocations) == 0 {
  656. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  657. return nil
  658. }
  659. // 3. Retrieve pre-computed allocation resource totals, which will be used
  660. // to compute idle coefficients, based on the ratio of an allocation's per-
  661. // resource cost to the per-resource totals of that allocation's cluster or
  662. // node. Whether to perform this operation based on cluster or node is an
  663. // option. (See IdleByNode documentation; defaults to idle-by-cluster.)
  664. var allocTotals map[string]*AllocationTotals
  665. var ok bool
  666. if options.AllocationTotalsStore != nil {
  667. if options.IdleByNode {
  668. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByNode(*sas.Window.Start(), *sas.Window.End())
  669. if !ok {
  670. return fmt.Errorf("nil allocation resource totals by node for %s", sas.Window)
  671. }
  672. } else {
  673. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByCluster(*sas.Window.Start(), *sas.Window.End())
  674. if !ok {
  675. return fmt.Errorf("nil allocation resource totals by cluster for %s", sas.Window)
  676. }
  677. }
  678. }
  679. // If reconciliation has been fully or partially disabled, clear the
  680. // relevant adjustments from the alloc totals
  681. if allocTotals != nil && (!options.Reconcile || !options.ReconcileNetwork) {
  682. if !options.Reconcile {
  683. for _, tot := range allocTotals {
  684. tot.ClearAdjustments()
  685. }
  686. } else if !options.ReconcileNetwork {
  687. for _, tot := range allocTotals {
  688. tot.NetworkCostAdjustment = 0.0
  689. }
  690. }
  691. }
  692. // If filters have been applied, then we need to record allocation resource
  693. // totals after filtration (i.e. the allocations that are present) so that
  694. // we can identify the proportion of idle cost to keep. That is, we should
  695. // only return the idle cost that would be shared with the remaining
  696. // allocations, even if we're keeping idle separate. The totals should be
  697. // recorded by idle-key (cluster or node, depending on the IdleByNode
  698. // option). Instantiating this map is a signal to record the totals.
  699. var allocTotalsAfterFilters map[string]*AllocationTotals
  700. if len(resultSet.idleKeys) > 0 && options.Filter != nil {
  701. allocTotalsAfterFilters = make(map[string]*AllocationTotals, len(resultSet.idleKeys))
  702. }
  703. // If we're recording allocTotalsAfterFilters and there are shared costs,
  704. // then record those resource totals here so that idle for those shared
  705. // resources gets included.
  706. if allocTotalsAfterFilters != nil {
  707. for key, rt := range sharedResourceTotals {
  708. if _, ok := allocTotalsAfterFilters[key]; !ok {
  709. allocTotalsAfterFilters[key] = &AllocationTotals{}
  710. }
  711. // Record only those fields required for computing idle
  712. allocTotalsAfterFilters[key].CPUCost += rt.CPUCost
  713. allocTotalsAfterFilters[key].GPUCost += rt.GPUCost
  714. allocTotalsAfterFilters[key].RAMCost += rt.RAMCost
  715. }
  716. }
  717. // 4. Convert shared hourly cost into a cumulative allocation to share,
  718. // and insert it into the share set.
  719. for name, cost := range options.SharedHourlyCosts {
  720. if cost > 0.0 {
  721. hours := sas.Window.Hours()
  722. // If set ends in the future, adjust hours accordingly
  723. diff := time.Since(*sas.Window.End())
  724. if diff < 0.0 {
  725. hours += diff.Hours()
  726. }
  727. totalSharedCost := cost * hours
  728. shareSet.Insert(&SummaryAllocation{
  729. Name: fmt.Sprintf("%s/%s", name, SharedSuffix),
  730. Properties: &AllocationProperties{},
  731. Start: *sas.Window.Start(),
  732. End: *sas.Window.End(),
  733. SharedCost: totalSharedCost,
  734. })
  735. }
  736. }
  737. // Sharing coefficients are recorded by post-aggregation-key (e.g. if
  738. // aggregating by namespace, then the key will be the namespace) and only
  739. // need to be recorded if there are shared resources. Instantiating this
  740. // map is the signal to record sharing coefficients.
  741. var sharingCoeffs map[string]float64
  742. if len(shareSet.SummaryAllocations) > 0 {
  743. sharingCoeffs = map[string]float64{}
  744. }
  745. // Loop over all remaining SummaryAllocations (after filters, sharing, &c.)
  746. // doing the following, in this order:
  747. // 5. Compute sharing coefficients, if there are shared resources
  748. // 6. Distribute idle cost, if sharing idle
  749. // 7. Record allocTotalsAfterFiltration, if filters have been applied
  750. // 8. Aggregate by key
  751. for _, sa := range sas.SummaryAllocations {
  752. // Generate key to use for aggregation-by-key and allocation name
  753. key := sa.generateKey(aggregateBy, options.LabelConfig)
  754. // 5. Incrementally add to sharing coefficients before adding idle
  755. // cost, which would skew the coefficients. These coefficients will be
  756. // later divided by a total, turning them into a coefficient between
  757. // 0.0 and 1.0.
  758. // NOTE: SummaryAllocation does not support ShareEven, so only record
  759. // by cost for cost-weighted distribution.
  760. // if sharingCoeffs != nil {
  761. if sharingCoeffs != nil && !sa.IsUnmounted() {
  762. sharingCoeffs[key] += sa.TotalCost() - sa.SharedCost - sa.UnmountedPVCost
  763. }
  764. // 6. Distribute idle allocations according to the idle coefficients.
  765. // NOTE: if idle allocation is off (i.e. options.ShareIdle: ShareNone)
  766. // then all idle allocations will be in the resultSet at this point, so
  767. // idleSet will be empty and we won't enter this block.
  768. if len(idleSet.SummaryAllocations) > 0 {
  769. for _, idle := range idleSet.SummaryAllocations {
  770. // Idle key is either cluster or node, as determined by the
  771. // IdleByNode option.
  772. var key string
  773. // Only share idle allocation with current allocation (sa) if
  774. // the relevant properties match (i.e. cluster and/or node)
  775. if idle.Properties.Cluster != sa.Properties.Cluster {
  776. continue
  777. }
  778. key = idle.Properties.Cluster
  779. if options.IdleByNode {
  780. if idle.Properties.Node != sa.Properties.Node {
  781. continue
  782. }
  783. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  784. }
  785. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  786. sa.CPUCost += idle.CPUCost * cpuCoeff
  787. sa.GPUCost += idle.GPUCost * gpuCoeff
  788. sa.RAMCost += idle.RAMCost * ramCoeff
  789. }
  790. }
  791. // The key becomes the allocation's name, which is used as the key by
  792. // which the allocation is inserted into the set.
  793. sa.Name = key
  794. // If merging unallocated allocations, rename all unallocated
  795. // allocations as simply __unallocated__
  796. if options.MergeUnallocated && sa.IsUnallocated() {
  797. sa.Name = UnallocatedSuffix
  798. }
  799. // 7. Record filtered resource totals for idle allocation filtration,
  800. // only if necessary.
  801. if allocTotalsAfterFilters != nil {
  802. key := sa.Properties.Cluster
  803. if options.IdleByNode {
  804. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  805. }
  806. if _, ok := allocTotalsAfterFilters[key]; !ok {
  807. allocTotalsAfterFilters[key] = &AllocationTotals{}
  808. }
  809. allocTotalsAfterFilters[key].CPUCost += sa.CPUCost
  810. allocTotalsAfterFilters[key].GPUCost += sa.GPUCost
  811. allocTotalsAfterFilters[key].RAMCost += sa.RAMCost
  812. }
  813. // 8. Inserting the allocation with the generated key for a name
  814. // performs the actual aggregation step.
  815. resultSet.Insert(sa)
  816. }
  817. // 9. If idle is shared and resources are shared, it's probable that some
  818. // amount of idle cost will be shared with a shared resource. Distribute
  819. // that idle cost, if it exists, among the respective shared allocations
  820. // before sharing them with the aggregated allocations.
  821. if len(idleSet.SummaryAllocations) > 0 && len(shareSet.SummaryAllocations) > 0 {
  822. for _, sa := range shareSet.SummaryAllocations {
  823. for _, idle := range idleSet.SummaryAllocations {
  824. var key string
  825. // Only share idle allocation with current allocation (sa) if
  826. // the relevant property matches (i.e. Cluster or Node,
  827. // depending on which idle sharing option is selected)
  828. if options.IdleByNode {
  829. if idle.Properties.Cluster != sa.Properties.Cluster || idle.Properties.Node != sa.Properties.Node {
  830. continue
  831. }
  832. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  833. } else {
  834. if idle.Properties.Cluster != sa.Properties.Cluster {
  835. continue
  836. }
  837. key = idle.Properties.Cluster
  838. }
  839. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  840. sa.CPUCost += idle.CPUCost * cpuCoeff
  841. sa.GPUCost += idle.GPUCost * gpuCoeff
  842. sa.RAMCost += idle.RAMCost * ramCoeff
  843. }
  844. }
  845. }
  846. // 10. Apply idle filtration, which "filters" the idle cost, i.e. scales
  847. // idle allocation costs per-resource by the proportion of allocation
  848. // resources remaining after filtering. In effect, this returns only the
  849. // idle costs that would have been shared with the remaining allocations,
  850. // even if idle is kept separated.
  851. if allocTotalsAfterFilters != nil {
  852. for idleKey := range resultSet.idleKeys {
  853. ia := resultSet.SummaryAllocations[idleKey]
  854. var key string
  855. if options.IdleByNode {
  856. key = fmt.Sprintf("%s/%s", ia.Properties.Cluster, ia.Properties.Node)
  857. } else {
  858. key = ia.Properties.Cluster
  859. }
  860. // Percentage of idle that should remain after filters are applied,
  861. // which equals the proportion of filtered-to-actual cost.
  862. cpuFilterCoeff := 0.0
  863. if allocTotals[key].TotalCPUCost() > 0.0 {
  864. filteredAlloc, ok := allocTotalsAfterFilters[key]
  865. if ok {
  866. cpuFilterCoeff = filteredAlloc.TotalCPUCost() / allocTotals[key].TotalCPUCost()
  867. } else {
  868. cpuFilterCoeff = 0.0
  869. }
  870. }
  871. gpuFilterCoeff := 0.0
  872. if allocTotals[key].TotalGPUCost() > 0.0 {
  873. filteredAlloc, ok := allocTotalsAfterFilters[key]
  874. if ok {
  875. gpuFilterCoeff = filteredAlloc.TotalGPUCost() / allocTotals[key].TotalGPUCost()
  876. } else {
  877. gpuFilterCoeff = 0.0
  878. }
  879. }
  880. ramFilterCoeff := 0.0
  881. if allocTotals[key].TotalRAMCost() > 0.0 {
  882. filteredAlloc, ok := allocTotalsAfterFilters[key]
  883. if ok {
  884. ramFilterCoeff = filteredAlloc.TotalRAMCost() / allocTotals[key].TotalRAMCost()
  885. } else {
  886. ramFilterCoeff = 0.0
  887. }
  888. }
  889. ia.CPUCost *= cpuFilterCoeff
  890. ia.GPUCost *= gpuFilterCoeff
  891. ia.RAMCost *= ramFilterCoeff
  892. }
  893. }
  894. // 11. Distribute shared resources according to sharing coefficients.
  895. // NOTE: ShareEven is not supported
  896. if len(shareSet.SummaryAllocations) > 0 {
  897. shareCoeffSum := 0.0
  898. sharingCoeffDenominator := 0.0
  899. for _, rt := range allocTotals {
  900. // Here, the allocation totals
  901. sharingCoeffDenominator += rt.TotalCost() // does NOT include unmounted PVs at all
  902. }
  903. // Do not include the shared costs, themselves, when determining
  904. // sharing coefficients.
  905. for _, rt := range sharedResourceTotals {
  906. // Due to the fact that sharingCoeffDenominator already has no
  907. // unmounted PV costs, we need to be careful not to additionally
  908. // subtract the unmounted PV cost when we remove shared costs
  909. // from the denominator.
  910. sharingCoeffDenominator -= (rt.TotalCost() - rt.UnmountedPVCost)
  911. }
  912. if sharingCoeffDenominator <= 0.0 {
  913. log.Warnf("SummaryAllocation: sharing coefficient denominator is %f", sharingCoeffDenominator)
  914. } else {
  915. // Compute sharing coeffs by dividing the thus-far accumulated
  916. // numerators by the now-finalized denominator.
  917. for key := range sharingCoeffs {
  918. // Do not share the value with unmounted suffix since it's not included in the computation.
  919. if key == UnmountedSuffix {
  920. continue
  921. }
  922. if sharingCoeffs[key] > 0.0 {
  923. sharingCoeffs[key] /= sharingCoeffDenominator
  924. shareCoeffSum += sharingCoeffs[key]
  925. } else {
  926. log.Warnf("SummaryAllocation: detected illegal sharing coefficient for %s: %v (setting to zero)", key, sharingCoeffs[key])
  927. sharingCoeffs[key] = 0.0
  928. }
  929. }
  930. for key, sa := range resultSet.SummaryAllocations {
  931. // Idle and unmounted allocations, by definition, do not
  932. // receive shared cost
  933. if sa.IsIdle() || sa.IsUnmounted() {
  934. continue
  935. }
  936. sharingCoeff := sharingCoeffs[key]
  937. // Distribute each shared cost with the current allocation on the
  938. // basis of the proportion of the allocation's cost (ShareWeighted)
  939. // or count (ShareEven) to the total aggregated cost or count. This
  940. // condition should hold in spite of filters because the sharing
  941. // coefficient denominator is held constant by pre-computed
  942. // resource totals and the post-aggregation total cost of the
  943. // remaining allocations will, by definition, not be affected.
  944. for _, shared := range shareSet.SummaryAllocations {
  945. sa.SharedCost += shared.TotalCost() * sharingCoeff
  946. }
  947. }
  948. }
  949. }
  950. // 12. Insert external allocations into the result set.
  951. for _, sa := range externalSet.SummaryAllocations {
  952. // Make an allocation with the same properties and test that
  953. // against the FilterFunc to see if the external allocation should
  954. // be filtered or not.
  955. // TODO:CLEANUP do something about external cost, this stinks
  956. ea := &Allocation{Properties: sa.Properties}
  957. if filter.Matches(ea) {
  958. key := sa.generateKey(aggregateBy, options.LabelConfig)
  959. sa.Name = key
  960. resultSet.Insert(sa)
  961. }
  962. }
  963. // 13. Distribute remaining, undistributed idle. Undistributed idle is any
  964. // per-resource idle cost for which there can be no idle coefficient
  965. // computed because there is zero usage across all allocations.
  966. for _, isa := range idleSet.SummaryAllocations {
  967. // Make an allocation with the same properties and test that
  968. // against the FilterFunc to see if the external allocation should
  969. // be filtered or not.
  970. // TODO:CLEANUP do something about external cost, this stinks
  971. ia := &Allocation{Properties: isa.Properties}
  972. // if the idle does not apply to the non-filtered values, skip it
  973. if !filter.Matches(ia) {
  974. continue
  975. }
  976. key := isa.Properties.Cluster
  977. if options.IdleByNode {
  978. key = fmt.Sprintf("%s/%s", isa.Properties.Cluster, isa.Properties.Node)
  979. }
  980. rt, ok := allocTotals[key]
  981. if !ok {
  982. log.Warnf("SummaryAllocation: AggregateBy: cannot handle undistributed idle for '%s'", key)
  983. continue
  984. }
  985. hasUndistributableCost := false
  986. if isa.CPUCost > 0.0 && rt.CPUCost == 0.0 {
  987. // There is idle CPU cost, but no allocated CPU cost, so that cost
  988. // is undistributable and must be inserted.
  989. hasUndistributableCost = true
  990. } else {
  991. // Cost was entirely distributed, so zero it out
  992. isa.CPUCost = 0.0
  993. }
  994. if isa.GPUCost > 0.0 && rt.GPUCost == 0.0 {
  995. // There is idle GPU cost, but no allocated GPU cost, so that cost
  996. // is undistributable and must be inserted.
  997. hasUndistributableCost = true
  998. } else {
  999. // Cost was entirely distributed, so zero it out
  1000. isa.GPUCost = 0.0
  1001. }
  1002. if isa.RAMCost > 0.0 && rt.RAMCost == 0.0 {
  1003. // There is idle CPU cost, but no allocated CPU cost, so that cost
  1004. // is undistributable and must be inserted.
  1005. hasUndistributableCost = true
  1006. } else {
  1007. // Cost was entirely distributed, so zero it out
  1008. isa.RAMCost = 0.0
  1009. }
  1010. if hasUndistributableCost {
  1011. isa.Name = fmt.Sprintf("%s/%s", key, IdleSuffix)
  1012. resultSet.Insert(isa)
  1013. }
  1014. }
  1015. // 14. Combine all idle allocations into a single idle allocation, unless
  1016. // the option to keep idle split by cluster or node is enabled.
  1017. if !options.SplitIdle {
  1018. for _, ia := range resultSet.idleAllocations() {
  1019. resultSet.Delete(ia.Name)
  1020. ia.Name = IdleSuffix
  1021. resultSet.Insert(ia)
  1022. }
  1023. }
  1024. // Replace the existing set's data with the new, aggregated summary data
  1025. sas.SummaryAllocations = resultSet.SummaryAllocations
  1026. return nil
  1027. }
  1028. // Delete removes the allocation with the given name from the set
  1029. func (sas *SummaryAllocationSet) Delete(name string) {
  1030. if sas == nil {
  1031. return
  1032. }
  1033. sas.Lock()
  1034. defer sas.Unlock()
  1035. delete(sas.externalKeys, name)
  1036. delete(sas.idleKeys, name)
  1037. delete(sas.SummaryAllocations, name)
  1038. }
  1039. // Each invokes the given function for each SummaryAllocation in the set
  1040. func (sas *SummaryAllocationSet) Each(f func(string, *SummaryAllocation)) {
  1041. if sas == nil {
  1042. return
  1043. }
  1044. for k, a := range sas.SummaryAllocations {
  1045. f(k, a)
  1046. }
  1047. }
  1048. func (sas *SummaryAllocationSet) Equal(that *SummaryAllocationSet) bool {
  1049. if sas == nil || that == nil {
  1050. return false
  1051. }
  1052. sas.RLock()
  1053. defer sas.RUnlock()
  1054. if !sas.Window.Equal(that.Window) {
  1055. return false
  1056. }
  1057. if len(sas.SummaryAllocations) != len(that.SummaryAllocations) {
  1058. return false
  1059. }
  1060. for name, sa := range sas.SummaryAllocations {
  1061. thatSA, ok := that.SummaryAllocations[name]
  1062. if !ok {
  1063. return false
  1064. }
  1065. if !sa.Equal(thatSA) {
  1066. return false
  1067. }
  1068. }
  1069. return true
  1070. }
  1071. // IdleAllocations returns a map of the idle allocations in the AllocationSet.
  1072. func (sas *SummaryAllocationSet) idleAllocations() map[string]*SummaryAllocation {
  1073. idles := map[string]*SummaryAllocation{}
  1074. if sas == nil || len(sas.SummaryAllocations) == 0 {
  1075. return idles
  1076. }
  1077. sas.RLock()
  1078. defer sas.RUnlock()
  1079. for key := range sas.idleKeys {
  1080. if sa, ok := sas.SummaryAllocations[key]; ok {
  1081. idles[key] = sa
  1082. }
  1083. }
  1084. return idles
  1085. }
  1086. // Insert aggregates the current entry in the SummaryAllocationSet by the given Allocation,
  1087. // but only if the Allocation is valid, i.e. matches the SummaryAllocationSet's window. If
  1088. // there is no existing entry, one is created. Nil error response indicates success.
  1089. func (sas *SummaryAllocationSet) Insert(sa *SummaryAllocation) error {
  1090. if sas == nil {
  1091. return fmt.Errorf("cannot insert into nil SummaryAllocationSet")
  1092. }
  1093. if sa == nil {
  1094. return fmt.Errorf("cannot insert a nil SummaryAllocation")
  1095. }
  1096. sas.Lock()
  1097. defer sas.Unlock()
  1098. if sas.SummaryAllocations == nil {
  1099. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  1100. }
  1101. if sas.externalKeys == nil {
  1102. sas.externalKeys = map[string]bool{}
  1103. }
  1104. if sas.idleKeys == nil {
  1105. sas.idleKeys = map[string]bool{}
  1106. }
  1107. // Add the given Allocation to the existing entry, if there is one;
  1108. // otherwise just set directly into allocations
  1109. if _, ok := sas.SummaryAllocations[sa.Name]; ok {
  1110. err := sas.SummaryAllocations[sa.Name].Add(sa)
  1111. if err != nil {
  1112. return fmt.Errorf("SummaryAllocationSet.Insert: error trying to Add: %s", err)
  1113. }
  1114. } else {
  1115. sas.SummaryAllocations[sa.Name] = sa
  1116. }
  1117. // If the given Allocation is an external one, record that
  1118. if sa.IsExternal() {
  1119. sas.externalKeys[sa.Name] = true
  1120. }
  1121. // If the given Allocation is an idle one, record that
  1122. if sa.IsIdle() {
  1123. sas.idleKeys[sa.Name] = true
  1124. }
  1125. return nil
  1126. }
  1127. func (sas *SummaryAllocationSet) TotalCost() float64 {
  1128. if sas == nil {
  1129. return 0.0
  1130. }
  1131. sas.RLock()
  1132. defer sas.RUnlock()
  1133. tc := 0.0
  1134. for _, sa := range sas.SummaryAllocations {
  1135. tc += sa.TotalCost()
  1136. }
  1137. return tc
  1138. }
  1139. // RAMEfficiency func to calculate average RAM efficiency over SummaryAllocationSet
  1140. func (sas *SummaryAllocationSet) RAMEfficiency() float64 {
  1141. if sas == nil {
  1142. return 0.0
  1143. }
  1144. sas.RLock()
  1145. defer sas.RUnlock()
  1146. totalRAMBytesMinutesUsage := 0.0
  1147. totalRAMBytesMinutesRequest := 0.0
  1148. totalRAMCost := 0.0
  1149. for _, sa := range sas.SummaryAllocations {
  1150. if sa.IsIdle() {
  1151. continue
  1152. }
  1153. totalRAMBytesMinutesUsage += sa.RAMBytesUsageAverage * sa.Minutes()
  1154. totalRAMBytesMinutesRequest += sa.RAMBytesRequestAverage * sa.Minutes()
  1155. totalRAMCost += sa.RAMCost
  1156. }
  1157. if totalRAMBytesMinutesRequest > 0 {
  1158. return totalRAMBytesMinutesUsage / totalRAMBytesMinutesRequest
  1159. }
  1160. if totalRAMBytesMinutesUsage == 0.0 || totalRAMCost == 0.0 {
  1161. return 0.0
  1162. }
  1163. return 1.0
  1164. }
  1165. // CPUEfficiency func to calculate average CPU efficiency over SummaryAllocationSet
  1166. func (sas *SummaryAllocationSet) CPUEfficiency() float64 {
  1167. if sas == nil {
  1168. return 0.0
  1169. }
  1170. sas.RLock()
  1171. defer sas.RUnlock()
  1172. totalCPUCoreMinutesUsage := 0.0
  1173. totalCPUCoreMinutesRequest := 0.0
  1174. totalCPUCost := 0.0
  1175. for _, sa := range sas.SummaryAllocations {
  1176. if sa.IsIdle() {
  1177. continue
  1178. }
  1179. totalCPUCoreMinutesUsage += sa.CPUCoreUsageAverage * sa.Minutes()
  1180. totalCPUCoreMinutesRequest += sa.CPUCoreRequestAverage * sa.Minutes()
  1181. totalCPUCost += sa.CPUCost
  1182. }
  1183. if totalCPUCoreMinutesRequest > 0 {
  1184. return totalCPUCoreMinutesUsage / totalCPUCoreMinutesRequest
  1185. }
  1186. if totalCPUCoreMinutesUsage == 0.0 || totalCPUCost == 0.0 {
  1187. return 0.0
  1188. }
  1189. return 1.0
  1190. }
  1191. // TotalEfficiency func to calculate average Total efficiency over SummaryAllocationSet
  1192. func (sas *SummaryAllocationSet) TotalEfficiency() float64 {
  1193. if sas == nil {
  1194. return 0.0
  1195. }
  1196. sas.RLock()
  1197. defer sas.RUnlock()
  1198. totalRAMCost := 0.0
  1199. totalCPUCost := 0.0
  1200. for _, sa := range sas.SummaryAllocations {
  1201. if sa.IsIdle() {
  1202. continue
  1203. }
  1204. totalRAMCost += sa.RAMCost
  1205. totalCPUCost += sa.CPUCost
  1206. }
  1207. if totalRAMCost+totalCPUCost > 0 {
  1208. return (totalRAMCost*sas.RAMEfficiency() + totalCPUCost*sas.CPUEfficiency()) / (totalRAMCost + totalCPUCost)
  1209. }
  1210. return 0.0
  1211. }
  1212. // SummaryAllocationSetRange is a thread-safe slice of SummaryAllocationSets.
  1213. type SummaryAllocationSetRange struct {
  1214. sync.RWMutex
  1215. Step time.Duration `json:"step"`
  1216. SummaryAllocationSets []*SummaryAllocationSet `json:"sets"`
  1217. Window Window `json:"window"`
  1218. Message string `json:"-"`
  1219. }
  1220. // NewSummaryAllocationSetRange instantiates a new range composed of the given
  1221. // SummaryAllocationSets in the order provided. The expectations about the
  1222. // SummaryAllocationSets are as follows:
  1223. // - window durations are all equal
  1224. // - sets are consecutive (i.e. chronologically sorted)
  1225. // - there are no gaps between sets
  1226. // - sets do not have overlapping windows
  1227. func NewSummaryAllocationSetRange(sass ...*SummaryAllocationSet) *SummaryAllocationSetRange {
  1228. var step time.Duration
  1229. window := NewWindow(nil, nil)
  1230. for _, sas := range sass {
  1231. if window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*window.Start())) {
  1232. window.start = sas.Window.Start()
  1233. }
  1234. if window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*window.End())) {
  1235. window.end = sas.Window.End()
  1236. }
  1237. if step == 0 {
  1238. step = sas.Window.Duration()
  1239. } else if step != sas.Window.Duration() {
  1240. log.Warnf("instantiating range with step %s using set of step %s is illegal", step, sas.Window.Duration())
  1241. }
  1242. }
  1243. return &SummaryAllocationSetRange{
  1244. Step: step,
  1245. SummaryAllocationSets: sass,
  1246. Window: window,
  1247. }
  1248. }
  1249. // Accumulate sums each AllocationSet in the given range, returning a single cumulative
  1250. // AllocationSet for the entire range.
  1251. func (sasr *SummaryAllocationSetRange) accumulate() (*SummaryAllocationSet, error) {
  1252. var result *SummaryAllocationSet
  1253. var err error
  1254. sasr.RLock()
  1255. defer sasr.RUnlock()
  1256. for _, sas := range sasr.SummaryAllocationSets {
  1257. result, err = result.Add(sas)
  1258. if err != nil {
  1259. return nil, err
  1260. }
  1261. }
  1262. return result, nil
  1263. }
  1264. // newAccumulation clones the first available SummaryAllocationSet to use as the data structure to
  1265. // accumulate the remaining data. This leaves the original SummaryAllocationSetRange intact.
  1266. func (sasr *SummaryAllocationSetRange) newAccumulation() (*SummaryAllocationSet, error) {
  1267. var result *SummaryAllocationSet
  1268. var err error
  1269. sasr.RLock()
  1270. defer sasr.RUnlock()
  1271. for _, sas := range sasr.SummaryAllocationSets {
  1272. // we want to clone the first summary allocation set, then just Add the others
  1273. // to the clone. We will eventually use the clone to create the set range.
  1274. if result == nil {
  1275. result = sas.Clone()
  1276. continue
  1277. }
  1278. // Copy if sas is non-nil
  1279. var sasCopy *SummaryAllocationSet = nil
  1280. if sas != nil {
  1281. sasCopy = sas.Clone()
  1282. }
  1283. // nil is ok to pass into Add
  1284. result, err = result.Add(sasCopy)
  1285. if err != nil {
  1286. return nil, err
  1287. }
  1288. }
  1289. return result, nil
  1290. }
  1291. // AggregateBy aggregates each AllocationSet in the range by the given
  1292. // properties and options.
  1293. func (sasr *SummaryAllocationSetRange) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  1294. sasr.Lock()
  1295. defer sasr.Unlock()
  1296. for _, sas := range sasr.SummaryAllocationSets {
  1297. err := sas.AggregateBy(aggregateBy, options)
  1298. if err != nil {
  1299. // Wipe out data so that corrupt data cannot be mistakenly used
  1300. sasr.SummaryAllocationSets = []*SummaryAllocationSet{}
  1301. return err
  1302. }
  1303. }
  1304. return nil
  1305. }
  1306. // Append appends the given AllocationSet to the end of the range. It does not
  1307. // validate whether or not that violates window continuity.
  1308. func (sasr *SummaryAllocationSetRange) Append(sas *SummaryAllocationSet) {
  1309. sasr.Lock()
  1310. defer sasr.Unlock()
  1311. // Append to list of sets
  1312. sasr.SummaryAllocationSets = append(sasr.SummaryAllocationSets, sas)
  1313. // Set step, if not set
  1314. if sasr.Step == 0 {
  1315. sasr.Step = sas.Window.Duration()
  1316. }
  1317. // Adjust window
  1318. if sasr.Window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*sasr.Window.Start())) {
  1319. sasr.Window.start = sas.Window.Start()
  1320. }
  1321. if sasr.Window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*sasr.Window.End())) {
  1322. sasr.Window.end = sas.Window.End()
  1323. }
  1324. }
  1325. // Each invokes the given function for each AllocationSet in the range
  1326. func (sasr *SummaryAllocationSetRange) Each(f func(int, *SummaryAllocationSet)) {
  1327. if sasr == nil {
  1328. return
  1329. }
  1330. for i, as := range sasr.SummaryAllocationSets {
  1331. f(i, as)
  1332. }
  1333. }
  1334. // InsertExternalAllocations takes all allocations in the given
  1335. // AllocationSetRange (they should all be considered "external") and inserts
  1336. // them into the receiving SummaryAllocationSetRange.
  1337. // TODO:CLEANUP replace this with a better idea (or get rid of external
  1338. // allocations, as such, altogether)
  1339. func (sasr *SummaryAllocationSetRange) InsertExternalAllocations(that *AllocationSetRange) error {
  1340. if sasr == nil {
  1341. return fmt.Errorf("cannot insert range into nil AllocationSetRange")
  1342. }
  1343. // keys maps window to index in range
  1344. keys := map[string]int{}
  1345. for i, as := range sasr.SummaryAllocationSets {
  1346. if as == nil {
  1347. continue
  1348. }
  1349. keys[as.Window.String()] = i
  1350. }
  1351. // Nothing to merge, so simply return
  1352. if len(keys) == 0 {
  1353. return nil
  1354. }
  1355. var err error
  1356. for _, thatAS := range that.Allocations {
  1357. if thatAS == nil || err != nil {
  1358. continue
  1359. }
  1360. // Find matching AllocationSet in asr
  1361. i, ok := keys[thatAS.Window.String()]
  1362. if !ok {
  1363. err = fmt.Errorf("cannot merge AllocationSet into window that does not exist: %s", thatAS.Window.String())
  1364. continue
  1365. }
  1366. sas := sasr.SummaryAllocationSets[i]
  1367. // Insert each Allocation from the given set
  1368. for _, alloc := range thatAS.Allocations {
  1369. externalSA := NewSummaryAllocation(alloc, true, true)
  1370. // This error will be returned below
  1371. // TODO:CLEANUP should Each have early-error-return functionality?
  1372. err = sas.Insert(externalSA)
  1373. }
  1374. }
  1375. // err might be nil
  1376. return err
  1377. }
  1378. func (sasr *SummaryAllocationSetRange) TotalCost() float64 {
  1379. if sasr == nil {
  1380. return 0.0
  1381. }
  1382. sasr.RLock()
  1383. defer sasr.RUnlock()
  1384. tc := 0.0
  1385. for _, sas := range sasr.SummaryAllocationSets {
  1386. tc += sas.TotalCost()
  1387. }
  1388. return tc
  1389. }
  1390. // TODO remove after testing
  1391. func (sasr *SummaryAllocationSetRange) Print(verbose bool) {
  1392. fmt.Printf("%s (dur=%s, len=%d, cost=%.5f)\n", sasr.Window, sasr.Window.Duration(), len(sasr.SummaryAllocationSets), sasr.TotalCost())
  1393. for _, sas := range sasr.SummaryAllocationSets {
  1394. fmt.Printf(" > %s (dur=%s, len=%d, cost=%.5f) \n", sas.Window, sas.Window.Duration(), len(sas.SummaryAllocations), sas.TotalCost())
  1395. for key, sa := range sas.SummaryAllocations {
  1396. if verbose {
  1397. fmt.Printf(" {\"%s\", cpu: %.5f, gpu: %.5f, lb: %.5f, net: %.5f, pv: %.5f, ram: %.5f, shared: %.5f, external: %.5f}\n",
  1398. key, sa.CPUCost, sa.GPUCost, sa.LoadBalancerCost, sa.NetworkCost, sa.PVCost, sa.RAMCost, sa.SharedCost, sa.ExternalCost)
  1399. } else {
  1400. fmt.Printf(" - \"%s\": %.5f\n", key, sa.TotalCost())
  1401. }
  1402. }
  1403. }
  1404. }
  1405. func (sasr *SummaryAllocationSetRange) Accumulate(accumulateBy AccumulateOption) (*SummaryAllocationSetRange, error) {
  1406. switch accumulateBy {
  1407. case AccumulateOptionNone:
  1408. return sasr.accumulateByNone()
  1409. case AccumulateOptionAll:
  1410. return sasr.accumulateByAll()
  1411. case AccumulateOptionHour:
  1412. return sasr.accumulateByHour()
  1413. case AccumulateOptionDay:
  1414. return sasr.accumulateByDay()
  1415. case AccumulateOptionWeek:
  1416. return sasr.accumulateByWeek()
  1417. case AccumulateOptionMonth:
  1418. return sasr.accumulateByMonth()
  1419. default:
  1420. // this should never happen
  1421. return nil, fmt.Errorf("unexpected error, invalid accumulateByType: %s", accumulateBy)
  1422. }
  1423. }
  1424. func (sasr *SummaryAllocationSetRange) accumulateByNone() (*SummaryAllocationSetRange, error) {
  1425. return sasr.clone(), nil
  1426. }
  1427. func (sasr *SummaryAllocationSetRange) accumulateByAll() (*SummaryAllocationSetRange, error) {
  1428. var err error
  1429. var result *SummaryAllocationSet
  1430. result, err = sasr.newAccumulation()
  1431. if err != nil {
  1432. return nil, fmt.Errorf("error running accumulate: %s", err)
  1433. }
  1434. accumulated := NewSummaryAllocationSetRange(result)
  1435. return accumulated, nil
  1436. }
  1437. func (sasr *SummaryAllocationSetRange) accumulateByHour() (*SummaryAllocationSetRange, error) {
  1438. // ensure that the summary allocation sets have a 1-hour window, if a set exists
  1439. if len(sasr.SummaryAllocationSets) > 0 && sasr.SummaryAllocationSets[0].Window.Duration() != time.Hour {
  1440. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", sasr.SummaryAllocationSets[0].Window.Duration())
  1441. }
  1442. result := sasr.clone()
  1443. return result, nil
  1444. }
  1445. func (sasr *SummaryAllocationSetRange) accumulateByDay() (*SummaryAllocationSetRange, error) {
  1446. // if the summary allocation set window is 1-day, just return the existing summary allocation set range
  1447. if len(sasr.SummaryAllocationSets) > 0 && sasr.SummaryAllocationSets[0].Window.Duration() == time.Hour*24 {
  1448. return sasr, nil
  1449. }
  1450. var toAccumulate *SummaryAllocationSetRange
  1451. result := NewSummaryAllocationSetRange()
  1452. for i, as := range sasr.SummaryAllocationSets {
  1453. if as.Window.Duration() != time.Hour {
  1454. return nil, fmt.Errorf("window duration must equal 1 hour; got:%s", as.Window.Duration())
  1455. }
  1456. hour := as.Window.Start().Hour()
  1457. if toAccumulate == nil {
  1458. toAccumulate = NewSummaryAllocationSetRange()
  1459. as = as.Clone()
  1460. }
  1461. toAccumulate.Append(as)
  1462. sas, err := toAccumulate.accumulate()
  1463. if err != nil {
  1464. return nil, fmt.Errorf("error accumulating result: %s", err)
  1465. }
  1466. toAccumulate = NewSummaryAllocationSetRange(sas)
  1467. if hour == 23 || i == len(sasr.SummaryAllocationSets)-1 {
  1468. if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
  1469. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  1470. }
  1471. result.Append(toAccumulate.SummaryAllocationSets[0])
  1472. toAccumulate = nil
  1473. }
  1474. }
  1475. return result, nil
  1476. }
  1477. func (sasr *SummaryAllocationSetRange) accumulateByMonth() (*SummaryAllocationSetRange, error) {
  1478. var toAccumulate *SummaryAllocationSetRange
  1479. result := NewSummaryAllocationSetRange()
  1480. for i, as := range sasr.SummaryAllocationSets {
  1481. if as.Window.Duration() != time.Hour*24 {
  1482. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
  1483. }
  1484. _, month, _ := as.Window.Start().Date()
  1485. _, nextDayMonth, _ := as.Window.Start().Add(time.Hour * 24).Date()
  1486. if toAccumulate == nil {
  1487. toAccumulate = NewSummaryAllocationSetRange()
  1488. as = as.Clone()
  1489. }
  1490. toAccumulate.Append(as)
  1491. sas, err := toAccumulate.accumulate()
  1492. if err != nil {
  1493. return nil, fmt.Errorf("error building monthly accumulation: %s", err)
  1494. }
  1495. toAccumulate = NewSummaryAllocationSetRange(sas)
  1496. // either the month has ended, or there are no more summary allocation sets
  1497. if month != nextDayMonth || i == len(sasr.SummaryAllocationSets)-1 {
  1498. if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
  1499. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  1500. }
  1501. result.Append(toAccumulate.SummaryAllocationSets[0])
  1502. toAccumulate = nil
  1503. }
  1504. }
  1505. return result, nil
  1506. }
  1507. func (sasr *SummaryAllocationSetRange) accumulateByWeek() (*SummaryAllocationSetRange, error) {
  1508. if len(sasr.SummaryAllocationSets) > 0 && sasr.SummaryAllocationSets[0].Window.Duration() == timeutil.Week {
  1509. return sasr, nil
  1510. }
  1511. var toAccumulate *SummaryAllocationSetRange
  1512. result := NewSummaryAllocationSetRange()
  1513. for i, as := range sasr.SummaryAllocationSets {
  1514. if as.Window.Duration() != time.Hour*24 {
  1515. return nil, fmt.Errorf("window duration must equal 24 hours; got:%s", as.Window.Duration())
  1516. }
  1517. dayOfWeek := as.Window.Start().Weekday()
  1518. if toAccumulate == nil {
  1519. toAccumulate = NewSummaryAllocationSetRange()
  1520. as = as.Clone()
  1521. }
  1522. toAccumulate.Append(as)
  1523. sas, err := toAccumulate.accumulate()
  1524. if err != nil {
  1525. return nil, fmt.Errorf("error accumulating result: %s", err)
  1526. }
  1527. toAccumulate = NewSummaryAllocationSetRange(sas)
  1528. // current assumption is the week always ends on Saturday, or when there are no more summary allocation sets
  1529. if dayOfWeek == time.Saturday || i == len(sasr.SummaryAllocationSets)-1 {
  1530. if length := len(toAccumulate.SummaryAllocationSets); length != 1 {
  1531. return nil, fmt.Errorf("failed accumulation, detected %d sets instead of 1", length)
  1532. }
  1533. result.Append(toAccumulate.SummaryAllocationSets[0])
  1534. toAccumulate = nil
  1535. }
  1536. }
  1537. return result, nil
  1538. }
  1539. func (sasr *SummaryAllocationSetRange) Clone() *SummaryAllocationSetRange {
  1540. return sasr.clone()
  1541. }
  1542. // clone returns a new SummaryAllocationSetRange cloned from the existing SASR
  1543. func (sasr *SummaryAllocationSetRange) clone() *SummaryAllocationSetRange {
  1544. sasrSource := NewSummaryAllocationSetRange()
  1545. sasrSource.Window = sasr.Window.Clone()
  1546. sasrSource.Step = sasr.Step
  1547. sasrSource.Message = sasr.Message
  1548. for _, sas := range sasr.SummaryAllocationSets {
  1549. var sasClone *SummaryAllocationSet = nil
  1550. if sas != nil {
  1551. sasClone = sas.Clone()
  1552. }
  1553. sasrSource.Append(sasClone)
  1554. }
  1555. return sasrSource
  1556. }