summaryallocation.go 56 KB

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