summaryallocation.go 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254
  1. package kubecost
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "sync"
  7. "time"
  8. "github.com/kubecost/cost-model/pkg/log"
  9. )
  10. // SummaryAllocation summarizes an Allocation, keeping only fields necessary
  11. // for providing a high-level view of identifying the Allocation over a period
  12. // of time (Start, End) over which it ran, and inspecting the associated per-
  13. // resource costs (subtotaled with adjustments), total cost, and efficiency.
  14. //
  15. // SummaryAllocation does not have a concept of Window (i.e. the time period
  16. // within which it is defined, as opposed to the Start and End times). That
  17. // context must be provided by a SummaryAllocationSet.
  18. type SummaryAllocation struct {
  19. Name string `json:"name"`
  20. Properties *AllocationProperties `json:"-"`
  21. Start time.Time `json:"start"`
  22. End time.Time `json:"end"`
  23. CPUCoreRequestAverage float64 `json:"cpuCoreRequestAverage"`
  24. CPUCoreUsageAverage float64 `json:"cpuCoreUsageAverage"`
  25. CPUCost float64 `json:"cpuCost"`
  26. GPUCost float64 `json:"gpuCost"`
  27. NetworkCost float64 `json:"networkCost"`
  28. LoadBalancerCost float64 `json:"loadBalancerCost"`
  29. PVCost float64 `json:"pvCost"`
  30. RAMBytesRequestAverage float64 `json:"ramByteRequestAverage"`
  31. RAMBytesUsageAverage float64 `json:"ramByteUsageAverage"`
  32. RAMCost float64 `json:"ramCost"`
  33. SharedCost float64 `json:"sharedCost"`
  34. ExternalCost float64 `json:"externalCost"`
  35. Share bool `json:"-"`
  36. }
  37. // NewSummaryAllocation converts an Allocation to a SummaryAllocation by
  38. // dropping unnecessary fields and consolidating others (e.g. adjustments).
  39. // Reconciliation happens here because that process is synonymous with the
  40. // consolidation of adjustment fields.
  41. func NewSummaryAllocation(alloc *Allocation, reconcile, reconcileNetwork bool) *SummaryAllocation {
  42. if alloc == nil {
  43. return nil
  44. }
  45. sa := &SummaryAllocation{
  46. Name: alloc.Name,
  47. Properties: alloc.Properties.Clone(),
  48. Start: alloc.Start,
  49. End: alloc.End,
  50. CPUCoreRequestAverage: alloc.CPUCoreRequestAverage,
  51. CPUCoreUsageAverage: alloc.CPUCoreUsageAverage,
  52. CPUCost: alloc.CPUCost + alloc.CPUCostAdjustment,
  53. GPUCost: alloc.GPUCost + alloc.GPUCostAdjustment,
  54. NetworkCost: alloc.NetworkCost + alloc.NetworkCostAdjustment,
  55. LoadBalancerCost: alloc.LoadBalancerCost + alloc.LoadBalancerCostAdjustment,
  56. PVCost: alloc.PVCost() + alloc.PVCostAdjustment,
  57. RAMBytesRequestAverage: alloc.RAMBytesRequestAverage,
  58. RAMBytesUsageAverage: alloc.RAMBytesUsageAverage,
  59. RAMCost: alloc.RAMCost + alloc.RAMCostAdjustment,
  60. SharedCost: alloc.SharedCost,
  61. ExternalCost: alloc.ExternalCost,
  62. }
  63. // Revert adjustments if reconciliation is off. If only network
  64. // reconciliation is off, only revert network adjustment.
  65. if !reconcile {
  66. sa.CPUCost -= alloc.CPUCostAdjustment
  67. sa.GPUCost -= alloc.GPUCostAdjustment
  68. sa.NetworkCost -= alloc.NetworkCostAdjustment
  69. sa.LoadBalancerCost -= alloc.LoadBalancerCostAdjustment
  70. sa.PVCost -= alloc.PVCostAdjustment
  71. sa.RAMCost -= alloc.RAMCostAdjustment
  72. } else if !reconcileNetwork {
  73. sa.NetworkCost -= alloc.NetworkCostAdjustment
  74. }
  75. return sa
  76. }
  77. // Add sums two SummaryAllocations, adding the given SummaryAllocation to the
  78. // receiving one, thus mutating the receiver. For performance reasons, it
  79. // simply drops Properties, so a SummaryAllocation can only be Added once.
  80. func (sa *SummaryAllocation) Add(that *SummaryAllocation) error {
  81. if sa == nil || that == nil {
  82. return errors.New("cannot Add a nil SummaryAllocation")
  83. }
  84. // Once Added, a SummaryAllocation has no Properties. This saves us from
  85. // having to compute the intersection of two sets of Properties, which is
  86. // expensive.
  87. sa.Properties = nil
  88. // Sum non-cumulative fields by turning them into cumulative, adding them,
  89. // and then converting them back into averages after minutes have been
  90. // combined (just below).
  91. cpuReqCoreMins := sa.CPUCoreRequestAverage * sa.Minutes()
  92. cpuReqCoreMins += that.CPUCoreRequestAverage * that.Minutes()
  93. cpuUseCoreMins := sa.CPUCoreUsageAverage * sa.Minutes()
  94. cpuUseCoreMins += that.CPUCoreUsageAverage * that.Minutes()
  95. ramReqByteMins := sa.RAMBytesRequestAverage * sa.Minutes()
  96. ramReqByteMins += that.RAMBytesRequestAverage * that.Minutes()
  97. ramUseByteMins := sa.RAMBytesUsageAverage * sa.Minutes()
  98. ramUseByteMins += that.RAMBytesUsageAverage * that.Minutes()
  99. // Expand Start and End to be the "max" of among the given Allocations
  100. if that.Start.Before(sa.Start) {
  101. sa.Start = that.Start
  102. }
  103. if that.End.After(sa.End) {
  104. sa.End = that.End
  105. }
  106. // Convert cumulative request and usage back into rates
  107. if sa.Minutes() > 0 {
  108. sa.CPUCoreRequestAverage = cpuReqCoreMins / sa.Minutes()
  109. sa.CPUCoreUsageAverage = cpuUseCoreMins / sa.Minutes()
  110. sa.RAMBytesRequestAverage = ramReqByteMins / sa.Minutes()
  111. sa.RAMBytesUsageAverage = ramUseByteMins / sa.Minutes()
  112. } else {
  113. sa.CPUCoreRequestAverage = 0.0
  114. sa.CPUCoreUsageAverage = 0.0
  115. sa.RAMBytesRequestAverage = 0.0
  116. sa.RAMBytesUsageAverage = 0.0
  117. }
  118. // Sum all cumulative cost fields
  119. sa.CPUCost += that.CPUCost
  120. sa.ExternalCost += that.ExternalCost
  121. sa.GPUCost += that.GPUCost
  122. sa.LoadBalancerCost += that.LoadBalancerCost
  123. sa.NetworkCost += that.NetworkCost
  124. sa.PVCost += that.PVCost
  125. sa.RAMCost += that.RAMCost
  126. sa.SharedCost += that.SharedCost
  127. return nil
  128. }
  129. // Clone copies the SummaryAllocation and returns the copy
  130. func (sa *SummaryAllocation) Clone() *SummaryAllocation {
  131. return &SummaryAllocation{
  132. Name: sa.Name,
  133. Properties: sa.Properties.Clone(),
  134. Start: sa.Start,
  135. End: sa.End,
  136. CPUCoreRequestAverage: sa.CPUCoreRequestAverage,
  137. CPUCoreUsageAverage: sa.CPUCoreUsageAverage,
  138. CPUCost: sa.CPUCost,
  139. GPUCost: sa.GPUCost,
  140. NetworkCost: sa.NetworkCost,
  141. LoadBalancerCost: sa.LoadBalancerCost,
  142. PVCost: sa.PVCost,
  143. RAMBytesRequestAverage: sa.RAMBytesRequestAverage,
  144. RAMBytesUsageAverage: sa.RAMBytesUsageAverage,
  145. RAMCost: sa.RAMCost,
  146. SharedCost: sa.SharedCost,
  147. ExternalCost: sa.ExternalCost,
  148. }
  149. }
  150. // CPUEfficiency is the ratio of usage to request. If there is no request and
  151. // no usage or cost, then efficiency is zero. If there is no request, but there
  152. // is usage or cost, then efficiency is 100%.
  153. func (sa *SummaryAllocation) CPUEfficiency() float64 {
  154. if sa == nil {
  155. return 0.0
  156. }
  157. if sa.CPUCoreRequestAverage > 0 {
  158. return sa.CPUCoreUsageAverage / sa.CPUCoreRequestAverage
  159. }
  160. if sa.CPUCoreUsageAverage == 0.0 || sa.CPUCost == 0.0 {
  161. return 0.0
  162. }
  163. return 1.0
  164. }
  165. func (sa *SummaryAllocation) generateKey(aggregateBy []string, labelConfig *LabelConfig) string {
  166. if sa == nil {
  167. return ""
  168. }
  169. return sa.Properties.GenerateKey(aggregateBy, labelConfig)
  170. }
  171. // IsExternal is true if the given SummaryAllocation represents external costs.
  172. func (sa *SummaryAllocation) IsExternal() bool {
  173. if sa == nil {
  174. return false
  175. }
  176. return strings.Contains(sa.Name, ExternalSuffix)
  177. }
  178. // IsIdle is true if the given SummaryAllocation represents idle costs.
  179. func (sa *SummaryAllocation) IsIdle() bool {
  180. if sa == nil {
  181. return false
  182. }
  183. return strings.Contains(sa.Name, IdleSuffix)
  184. }
  185. // IsUnallocated is true if the given SummaryAllocation represents unallocated
  186. // costs.
  187. func (sa *SummaryAllocation) IsUnallocated() bool {
  188. if sa == nil {
  189. return false
  190. }
  191. return strings.Contains(sa.Name, UnallocatedSuffix)
  192. }
  193. // IsUnmounted is true if the given SummaryAllocation represents unmounted
  194. // volume costs.
  195. func (sa *SummaryAllocation) IsUnmounted() bool {
  196. if sa == nil {
  197. return false
  198. }
  199. return strings.Contains(sa.Name, UnmountedSuffix)
  200. }
  201. // Minutes returns the number of minutes the SummaryAllocation represents, as
  202. // defined by the difference between the end and start times.
  203. func (sa *SummaryAllocation) Minutes() float64 {
  204. if sa == nil {
  205. return 0.0
  206. }
  207. return sa.End.Sub(sa.Start).Minutes()
  208. }
  209. // RAMEfficiency is the ratio of usage to request. If there is no request and
  210. // no usage or cost, then efficiency is zero. If there is no request, but there
  211. // is usage or cost, then efficiency is 100%.
  212. func (sa *SummaryAllocation) RAMEfficiency() float64 {
  213. if sa == nil {
  214. return 0.0
  215. }
  216. if sa.RAMBytesRequestAverage > 0 {
  217. return sa.RAMBytesUsageAverage / sa.RAMBytesRequestAverage
  218. }
  219. if sa.RAMBytesUsageAverage == 0.0 || sa.RAMCost == 0.0 {
  220. return 0.0
  221. }
  222. return 1.0
  223. }
  224. // TotalCost is the total cost of the SummaryAllocation
  225. func (sa *SummaryAllocation) TotalCost() float64 {
  226. if sa == nil {
  227. return 0.0
  228. }
  229. return sa.CPUCost + sa.GPUCost + sa.RAMCost + sa.PVCost + sa.NetworkCost + sa.LoadBalancerCost + sa.SharedCost + sa.ExternalCost
  230. }
  231. // TotalEfficiency is the cost-weighted average of CPU and RAM efficiency. If
  232. // there is no cost at all, then efficiency is zero.
  233. func (sa *SummaryAllocation) TotalEfficiency() float64 {
  234. if sa == nil {
  235. return 0.0
  236. }
  237. if sa.RAMCost+sa.CPUCost > 0 {
  238. ramCostEff := sa.RAMEfficiency() * sa.RAMCost
  239. cpuCostEff := sa.CPUEfficiency() * sa.CPUCost
  240. return (ramCostEff + cpuCostEff) / (sa.CPUCost + sa.RAMCost)
  241. }
  242. return 0.0
  243. }
  244. // SummaryAllocationSet stores a set of SummaryAllocations, each with a unique
  245. // name, that share a window. An AllocationSet is mutable, so treat it like a
  246. // threadsafe map.
  247. type SummaryAllocationSet struct {
  248. sync.RWMutex
  249. externalKeys map[string]bool
  250. idleKeys map[string]bool
  251. SummaryAllocations map[string]*SummaryAllocation `json:"allocations"`
  252. Window Window `json:"window"`
  253. }
  254. // NewSummaryAllocationSet converts an AllocationSet to a SummaryAllocationSet.
  255. // Filter functions, sharing functions, and reconciliation parameters are
  256. // required for unfortunate reasons to do with performance and legacy order-of-
  257. // operations details, as well as the fact that reconciliation has been
  258. // pushed down to the conversion step between Allocation and SummaryAllocation.
  259. func NewSummaryAllocationSet(as *AllocationSet, ffs, sfs []AllocationMatchFunc, reconcile, reconcileNetwork bool) *SummaryAllocationSet {
  260. if as == nil {
  261. return nil
  262. }
  263. // If we can know the exact size of the map, use it. If filters or sharing
  264. // functions are present, we can't know the size, so we make a default map.
  265. var sasMap map[string]*SummaryAllocation
  266. if len(ffs) == 0 && len(sfs) == 0 {
  267. // No filters, so make the map of summary allocations exactly the size
  268. // of the origin allocation set.
  269. sasMap = make(map[string]*SummaryAllocation, len(as.allocations))
  270. } else {
  271. // There are filters, so start with a standard map
  272. sasMap = make(map[string]*SummaryAllocation)
  273. }
  274. sas := &SummaryAllocationSet{
  275. SummaryAllocations: sasMap,
  276. Window: as.Window.Clone(),
  277. }
  278. for _, alloc := range as.allocations {
  279. // First, detect if the allocation should be shared. If so, mark it as
  280. // such, insert it, and continue.
  281. shouldShare := false
  282. for _, sf := range sfs {
  283. if sf(alloc) {
  284. shouldShare = true
  285. break
  286. }
  287. }
  288. if shouldShare {
  289. sa := NewSummaryAllocation(alloc, reconcile, reconcileNetwork)
  290. sa.Share = true
  291. sas.Insert(sa)
  292. continue
  293. }
  294. // If the allocation does not pass any of the given filter functions,
  295. // do not insert it into the set.
  296. shouldFilter := false
  297. for _, ff := range ffs {
  298. if !ff(alloc) {
  299. shouldFilter = true
  300. break
  301. }
  302. }
  303. if shouldFilter {
  304. continue
  305. }
  306. err := sas.Insert(NewSummaryAllocation(alloc, reconcile, reconcileNetwork))
  307. if err != nil {
  308. log.Errorf("SummaryAllocation: error inserting summary of %s", alloc.Name)
  309. }
  310. }
  311. for key := range as.externalKeys {
  312. sas.externalKeys[key] = true
  313. }
  314. for key := range as.idleKeys {
  315. sas.idleKeys[key] = true
  316. }
  317. return sas
  318. }
  319. // Add sums two SummaryAllocationSets, which Adds all SummaryAllocations in the
  320. // given SummaryAllocationSet to thier counterparts in the receiving set. Add
  321. // also expands the Window to include both constituent Windows, in the case
  322. // that Add is being used from accumulating (as opposed to aggregating). For
  323. // performance reasons, the function may return either a new set, or an
  324. // unmodified original, so it should not be assumed that the original sets are
  325. // safeuly usable after calling Add.
  326. func (sas *SummaryAllocationSet) Add(that *SummaryAllocationSet) (*SummaryAllocationSet, error) {
  327. if sas == nil || len(sas.SummaryAllocations) == 0 {
  328. return that, nil
  329. }
  330. if that == nil || len(that.SummaryAllocations) == 0 {
  331. return sas, nil
  332. }
  333. if sas.Window.IsOpen() {
  334. return nil, errors.New("cannot add a SummaryAllocationSet with an open window")
  335. }
  336. // Set start, end to min(start), max(end)
  337. start := *sas.Window.Start()
  338. end := *sas.Window.End()
  339. if that.Window.Start().Before(start) {
  340. start = *that.Window.Start()
  341. }
  342. if that.Window.End().After(end) {
  343. end = *that.Window.End()
  344. }
  345. acc := &SummaryAllocationSet{
  346. SummaryAllocations: make(map[string]*SummaryAllocation, len(sas.SummaryAllocations)),
  347. Window: NewClosedWindow(start, end),
  348. }
  349. sas.RLock()
  350. defer sas.RUnlock()
  351. that.RLock()
  352. defer that.RUnlock()
  353. for _, alloc := range sas.SummaryAllocations {
  354. err := acc.Insert(alloc)
  355. if err != nil {
  356. return nil, err
  357. }
  358. }
  359. for _, alloc := range that.SummaryAllocations {
  360. err := acc.Insert(alloc)
  361. if err != nil {
  362. return nil, err
  363. }
  364. }
  365. return acc, nil
  366. }
  367. // AggregateBy aggregates the Allocations in the given AllocationSet by the given
  368. // AllocationProperty. This will only be legal if the AllocationSet is divisible by the
  369. // given AllocationProperty; e.g. Containers can be divided by Namespace, but not vice-a-versa.
  370. func (sas *SummaryAllocationSet) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  371. if sas == nil || len(sas.SummaryAllocations) == 0 {
  372. return nil
  373. }
  374. if sas.Window.IsOpen() {
  375. return errors.New("cannot aggregate a SummaryAllocationSet with an open window")
  376. }
  377. if options == nil {
  378. options = &AllocationAggregationOptions{}
  379. }
  380. if options.LabelConfig == nil {
  381. options.LabelConfig = NewLabelConfig()
  382. }
  383. // Check if we have any work to do; if not, then early return. If
  384. // aggregateBy is nil, we don't aggregate anything. On the other hand,
  385. // an empty slice implies that we should aggregate everything. (See
  386. // generateKey for why that makes sense.)
  387. shouldAggregate := aggregateBy != nil
  388. shouldShare := len(options.SharedHourlyCosts) > 0 || len(options.ShareFuncs) > 0
  389. if !shouldAggregate && !shouldShare {
  390. return nil
  391. }
  392. // The order of operations for aggregating a SummaryAllotionSet is as
  393. // follows:
  394. //
  395. // 1. Partition external, idle, and shared allocations into separate sets.
  396. // Also, create the resultSet into which the results will be aggregated.
  397. //
  398. // 2. Record resource totals for shared costs and unmounted volumes so
  399. // that we can account for them in computing idle coefficients.
  400. //
  401. // 3. Retrieve pre-computed allocation resource totals, which will be used
  402. // to compute idle sharing coefficients.
  403. //
  404. // 4. Compute sharing coefficients per-aggregation, if sharing resources.
  405. //
  406. // 5. Distribute idle allocations according to the idle coefficients.
  407. //
  408. // 6. Record allocation resource totals (after filtration) if filters have
  409. // been applied. (Used for filtering proportional amount of idle.)
  410. //
  411. // 7. Generate aggregation key and insert allocation into the output set
  412. //
  413. // 8. If idle is shared and resources are shared, it's probable that some
  414. // amount of idle cost will be shared with a shared resource.
  415. // Distribute that idle cost, if it exists, among the respective shared
  416. // allocations before sharing them with the aggregated allocations.
  417. //
  418. // 9. Apply idle filtration, which "filters" the idle cost, or scales it
  419. // by the proportion of allocation resources remaining after filters
  420. // have been applied.
  421. //
  422. // 10. Convert shared hourly cost into a cumulative allocation to share,
  423. // and insert it into the share set.
  424. //
  425. // 11. Distribute shared resources according to sharing coefficients.
  426. //
  427. // 12. Insert external allocations into the result set.
  428. //
  429. // 13. Insert any undistributed idle, in the case that idle
  430. // coefficients end up being zero and some idle is not shared.
  431. //
  432. // 14. Combine all idle allocations into a single idle allocation, unless
  433. // the option to keep idle split by cluster or node is enabled.
  434. // 1. Partition external, idle, and shared allocations into separate sets.
  435. // Also, create the resultSet into which the results will be aggregated.
  436. // resultSet will collect the aggregated allocations
  437. resultSet := &SummaryAllocationSet{
  438. Window: sas.Window.Clone(),
  439. }
  440. // externalSet will collect external allocations
  441. externalSet := &SummaryAllocationSet{
  442. Window: sas.Window.Clone(),
  443. }
  444. // idleSet will be shared among resultSet after initial aggregation
  445. // is complete
  446. idleSet := &SummaryAllocationSet{
  447. Window: sas.Window.Clone(),
  448. }
  449. // shareSet will be shared among resultSet after initial aggregation
  450. // is complete
  451. shareSet := &SummaryAllocationSet{
  452. Window: sas.Window.Clone(),
  453. }
  454. sas.Lock()
  455. defer sas.Unlock()
  456. // 2. Record resource totals for shared costs, aggregating by cluster or by
  457. // node (depending on if idle is partitioned by cluster or node) so that we
  458. // can account for them in computing idle coefficients. Do the same for
  459. // unmounted volume costs, which only require a total cost.
  460. sharedResourceTotals := map[string]*AllocationTotals{}
  461. totalUnmountedCost := 0.0
  462. // 1 & 2. Identify set membership and aggregate aforementioned totals.
  463. for _, sa := range sas.SummaryAllocations {
  464. if sa.Share {
  465. var key string
  466. if options.IdleByNode {
  467. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  468. } else {
  469. key = sa.Properties.Cluster
  470. }
  471. if _, ok := sharedResourceTotals[key]; !ok {
  472. sharedResourceTotals[key] = &AllocationTotals{}
  473. }
  474. sharedResourceTotals[key].CPUCost += sa.CPUCost
  475. sharedResourceTotals[key].GPUCost += sa.GPUCost
  476. sharedResourceTotals[key].LoadBalancerCost += sa.LoadBalancerCost
  477. sharedResourceTotals[key].NetworkCost += sa.NetworkCost
  478. sharedResourceTotals[key].PersistentVolumeCost += sa.PVCost
  479. sharedResourceTotals[key].RAMCost += sa.RAMCost
  480. shareSet.Insert(sa)
  481. delete(sas.SummaryAllocations, sa.Name)
  482. continue
  483. }
  484. // External allocations get aggregated post-hoc (see step 6) and do
  485. // not necessarily contain complete sets of properties, so they are
  486. // moved to a separate AllocationSet.
  487. if sa.IsExternal() {
  488. delete(sas.externalKeys, sa.Name)
  489. delete(sas.SummaryAllocations, sa.Name)
  490. externalSet.Insert(sa)
  491. continue
  492. }
  493. // Idle allocations should be separated into idleSet if they are to be
  494. // shared later on. If they are not to be shared, then add them to the
  495. // resultSet like any other allocation.
  496. if sa.IsIdle() {
  497. delete(sas.idleKeys, sa.Name)
  498. delete(sas.SummaryAllocations, sa.Name)
  499. if options.ShareIdle == ShareEven || options.ShareIdle == ShareWeighted {
  500. idleSet.Insert(sa)
  501. } else {
  502. resultSet.Insert(sa)
  503. }
  504. continue
  505. }
  506. // Track total unmounted cost because it must be taken out of total
  507. // allocated costs for sharing coefficients.
  508. if sa.IsUnmounted() {
  509. totalUnmountedCost += sa.TotalCost()
  510. }
  511. }
  512. // It's possible that no more un-shared, non-idle, non-external allocations
  513. // remain at this point. This always results in an emptySet, so return early.
  514. if len(sas.SummaryAllocations) == 0 {
  515. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  516. return nil
  517. }
  518. // 3. Retrieve pre-computed allocation resource totals, which will be used
  519. // to compute idle coefficients, based on the ratio of an allocation's per-
  520. // resource cost to the per-resource totals of that allocation's cluster or
  521. // node. Whether to perform this operation based on cluster or node is an
  522. // option. (See IdleByNode documentation; defaults to idle-by-cluster.)
  523. var allocTotals map[string]*AllocationTotals
  524. var ok bool
  525. if options.IdleByNode {
  526. if options.AllocationTotalsStore != nil {
  527. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByNode(*sas.Window.Start(), *sas.Window.End())
  528. if !ok {
  529. return fmt.Errorf("nil allocation resource totals by node for %s", sas.Window)
  530. }
  531. }
  532. } else {
  533. if options.AllocationTotalsStore != nil {
  534. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByCluster(*sas.Window.Start(), *sas.Window.End())
  535. if !ok {
  536. return fmt.Errorf("nil allocation resource totals by cluster for %s", sas.Window)
  537. }
  538. }
  539. }
  540. // If filters have been applied, then we need to record allocation resource
  541. // totals after filtration (i.e. the allocations that are present) so that
  542. // we can identify the proportion of idle cost to keep. That is, we should
  543. // only return the idle cost that would be shared with the remaining
  544. // allocations, even if we're keeping idle separate. The totals should be
  545. // recorded by idle-key (cluster or node, depending on the IdleByNode
  546. // option). Instantiating this map is a signal to record the totals.
  547. var allocTotalsAfterFilters map[string]*AllocationTotals
  548. if len(resultSet.idleKeys) > 0 && len(options.FilterFuncs) > 0 {
  549. allocTotalsAfterFilters = make(map[string]*AllocationTotals, len(resultSet.idleKeys))
  550. }
  551. // If we're recording allocTotalsAfterFilters and there are shared costs,
  552. // then record those resource totals here so that idle for thpse shared
  553. // resources gets included.
  554. if allocTotalsAfterFilters != nil {
  555. for key, rt := range sharedResourceTotals {
  556. if _, ok := allocTotalsAfterFilters[key]; !ok {
  557. allocTotalsAfterFilters[key] = &AllocationTotals{}
  558. }
  559. // Record only those fields required for computing idle
  560. allocTotalsAfterFilters[key].CPUCost += rt.CPUCost
  561. allocTotalsAfterFilters[key].GPUCost += rt.GPUCost
  562. allocTotalsAfterFilters[key].RAMCost += rt.RAMCost
  563. }
  564. }
  565. // Sharing coefficients are recorded by post-aggregation-key (e.g. if
  566. // aggregating by namespace, then the key will be the namespace) and only
  567. // need to be recorded if there are shared resources. Instantiating this
  568. // map is the signal to record sharing coefficients.
  569. var sharingCoeffs map[string]float64
  570. if len(shareSet.SummaryAllocations) > 0 {
  571. sharingCoeffs = map[string]float64{}
  572. }
  573. // Loop over all remaining SummaryAllocations (after filters, sharing, &c.)
  574. // doing the following, in this order:
  575. // 4. Compute sharing coefficients, if there are shared resources
  576. // 5. Distribute idle cost, if sharing idle
  577. // 6. Record allocTotalsAfterFiltration, if filters have been applied
  578. // 7. Aggregate by key
  579. for _, sa := range sas.SummaryAllocations {
  580. // Generate key to use for aggregation-by-key and allocation name
  581. key := sa.generateKey(aggregateBy, options.LabelConfig)
  582. // 4. Incrementally add to sharing coefficients before adding idle
  583. // cost, which would skew the coefficients. These coefficients will be
  584. // later divided by a total, turning them into a coefficient between
  585. // 0.0 and 1.0.
  586. // NOTE: SummaryAllocation does not support ShareEven, so only record
  587. // by cost for cost-weighted distribution.
  588. if sharingCoeffs != nil {
  589. sharingCoeffs[key] += sa.TotalCost()
  590. }
  591. // 5. Distribute idle allocations according to the idle coefficients.
  592. // NOTE: if idle allocation is off (i.e. ShareIdle == ShareNone) then
  593. // all idle allocations will be in the resultSet at this point, so idleSet
  594. // will be empty and we won't enter this block.
  595. if len(idleSet.SummaryAllocations) > 0 {
  596. for _, idle := range idleSet.SummaryAllocations {
  597. // Idle key is either cluster or node, as determined by the
  598. // IdleByNode option.
  599. var key string
  600. // Only share idle allocation with current allocation (sa) if
  601. // the relevant properties match (i.e. cluster and/or node)
  602. if idle.Properties.Cluster != sa.Properties.Cluster {
  603. continue
  604. }
  605. key = idle.Properties.Cluster
  606. if options.IdleByNode {
  607. if idle.Properties.Node != sa.Properties.Node {
  608. continue
  609. }
  610. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  611. }
  612. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  613. sa.CPUCost += idle.CPUCost * cpuCoeff
  614. sa.GPUCost += idle.GPUCost * gpuCoeff
  615. sa.RAMCost += idle.RAMCost * ramCoeff
  616. }
  617. }
  618. // The key becomes the allocation's name, which is used as the key by
  619. // which the allocation is inserted into the set.
  620. sa.Name = key
  621. // If merging unallocated allocations, rename all unallocated
  622. // allocations as simply __unallocated__
  623. if options.MergeUnallocated && sa.IsUnallocated() {
  624. sa.Name = UnallocatedSuffix
  625. }
  626. // 6. Record filtered resource totals for idle allocation filtration,
  627. // only if necessary.
  628. if allocTotalsAfterFilters != nil {
  629. key := sa.Properties.Cluster
  630. if options.IdleByNode {
  631. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  632. }
  633. if _, ok := allocTotalsAfterFilters[key]; ok {
  634. allocTotalsAfterFilters[key].CPUCost += sa.CPUCost
  635. allocTotalsAfterFilters[key].GPUCost += sa.GPUCost
  636. allocTotalsAfterFilters[key].RAMCost += sa.RAMCost
  637. } else {
  638. allocTotalsAfterFilters[key] = &AllocationTotals{
  639. CPUCost: sa.CPUCost,
  640. GPUCost: sa.GPUCost,
  641. RAMCost: sa.RAMCost,
  642. }
  643. }
  644. }
  645. // 7. Inserting the allocation with the generated key for a name
  646. // performs the actual aggregation step.
  647. resultSet.Insert(sa)
  648. }
  649. // 8. If idle is shared and resources are shared, it's probable that some
  650. // amount of idle cost will be shared with a shared resource. Distribute
  651. // that idle cost, if it exists, among the respective shared allocations
  652. // before sharing them with the aggregated allocations.
  653. if len(idleSet.SummaryAllocations) > 0 && len(shareSet.SummaryAllocations) > 0 {
  654. for _, sa := range shareSet.SummaryAllocations {
  655. for _, idle := range idleSet.SummaryAllocations {
  656. var key string
  657. // Only share idle allocation with current allocation (sa) if
  658. // the relevant property matches (i.e. Cluster or Node,
  659. // depending on which idle sharing option is selected)
  660. if options.IdleByNode {
  661. if idle.Properties.Node != sa.Properties.Node {
  662. continue
  663. }
  664. key = idle.Properties.Node
  665. } else {
  666. if idle.Properties.Cluster != sa.Properties.Cluster {
  667. continue
  668. }
  669. key = idle.Properties.Cluster
  670. }
  671. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  672. sa.CPUCost += idle.CPUCost * cpuCoeff
  673. sa.GPUCost += idle.GPUCost * gpuCoeff
  674. sa.RAMCost += idle.RAMCost * ramCoeff
  675. }
  676. }
  677. }
  678. // 9. Apply idle filtration, which "filters" the idle cost, i.e. scales
  679. // idle allocation costs per-resource by the proportion of allocation
  680. // resources remaining after filtering. In effect, this returns only the
  681. // idle costs that would have been shared with the remaining allocations,
  682. // even if idle is kept separated.
  683. if allocTotalsAfterFilters != nil {
  684. for idleKey := range resultSet.idleKeys {
  685. ia := resultSet.SummaryAllocations[idleKey]
  686. var key string
  687. if options.IdleByNode {
  688. key = ia.Properties.Node
  689. } else {
  690. key = ia.Properties.Cluster
  691. }
  692. // Percentage of idle that should remain after filters are applied,
  693. // which equals the proportion of filtered-to-actual cost.
  694. cpuFilterCoeff := 0.0
  695. if allocTotals[key].CPUCost > 0.0 {
  696. cpuFilterCoeff = allocTotalsAfterFilters[key].CPUCost / allocTotals[key].CPUCost
  697. }
  698. gpuFilterCoeff := 0.0
  699. if allocTotals[key].RAMCost > 0.0 {
  700. gpuFilterCoeff = allocTotalsAfterFilters[key].RAMCost / allocTotals[key].RAMCost
  701. }
  702. ramFilterCoeff := 0.0
  703. if allocTotals[key].RAMCost > 0.0 {
  704. ramFilterCoeff = allocTotalsAfterFilters[key].RAMCost / allocTotals[key].RAMCost
  705. }
  706. ia.CPUCost *= cpuFilterCoeff
  707. ia.GPUCost *= gpuFilterCoeff
  708. ia.RAMCost *= ramFilterCoeff
  709. }
  710. }
  711. // 10. Convert shared hourly cost into a cumulative allocation to share,
  712. // and insert it into the share set.
  713. for name, cost := range options.SharedHourlyCosts {
  714. if cost > 0.0 {
  715. hours := sas.Window.Hours()
  716. // If set ends in the future, adjust hours accordingly
  717. diff := time.Since(*sas.Window.End())
  718. if diff < 0.0 {
  719. hours += diff.Hours()
  720. }
  721. totalSharedCost := cost * hours
  722. shareSet.Insert(&SummaryAllocation{
  723. Name: fmt.Sprintf("%s/%s", name, SharedSuffix),
  724. Start: *sas.Window.Start(),
  725. End: *sas.Window.End(),
  726. SharedCost: totalSharedCost,
  727. })
  728. }
  729. }
  730. // 11. Distribute shared resources according to sharing coefficients.
  731. // NOTE: ShareEven is not supported
  732. if len(shareSet.SummaryAllocations) > 0 {
  733. sharingCoeffDenominator := 0.0
  734. for _, rt := range allocTotals {
  735. sharingCoeffDenominator += rt.TotalCost()
  736. }
  737. // Do not include the shared costs, themselves, when determining
  738. // sharing coefficients.
  739. for _, rt := range sharedResourceTotals {
  740. sharingCoeffDenominator -= rt.TotalCost()
  741. }
  742. // Do not include the unmounted costs when determining sharing
  743. // coefficients becuase they do not receive shared costs.
  744. sharingCoeffDenominator -= totalUnmountedCost
  745. if sharingCoeffDenominator <= 0.0 {
  746. log.Warningf("SummaryAllocation: sharing coefficient denominator is %f", sharingCoeffDenominator)
  747. } else {
  748. // Compute sharing coeffs by dividing the thus-far accumulated
  749. // numerators by the now-finalized denominator.
  750. for key := range sharingCoeffs {
  751. sharingCoeffs[key] /= sharingCoeffDenominator
  752. }
  753. for key, sa := range resultSet.SummaryAllocations {
  754. // Idle and unmounted allocations, by definition, do not
  755. // receive shared cost
  756. if sa.IsIdle() || sa.IsUnmounted() {
  757. continue
  758. }
  759. sharingCoeff := sharingCoeffs[key]
  760. // Distribute each shared cost with the current allocation on the
  761. // basis of the proportion of the allocation's cost (ShareWeighted)
  762. // or count (ShareEven) to the total aggregated cost or count. This
  763. // condition should hold in spite of filters because the sharing
  764. // coefficient denominator is held constant by pre-computed
  765. // resource totals and the post-aggregation total cost of the
  766. // remaining allocations will, by definition, not be affected.
  767. for _, shared := range shareSet.SummaryAllocations {
  768. sa.SharedCost += shared.TotalCost() * sharingCoeff
  769. }
  770. }
  771. }
  772. }
  773. // 12. Insert external allocations into the result set.
  774. for _, sa := range externalSet.SummaryAllocations {
  775. skip := false
  776. for _, ff := range options.FilterFuncs {
  777. // Make an allocation with the same properties and test that
  778. // against the FilterFunc to see if the external allocation should
  779. // be filtered or not.
  780. // TODO:CLEANUP do something about external cost, this stinks
  781. ea := &Allocation{Properties: sa.Properties}
  782. if !ff(ea) {
  783. skip = true
  784. break
  785. }
  786. }
  787. if !skip {
  788. key := sa.generateKey(aggregateBy, options.LabelConfig)
  789. sa.Name = key
  790. resultSet.Insert(sa)
  791. }
  792. }
  793. // 13. Distribute remaining, undistributed idle. Undistributed idle is any
  794. // per-resource idle cost for which there can be no idle coefficient
  795. // computed because there is zero usage across all allocations.
  796. for _, ia := range idleSet.SummaryAllocations {
  797. key := ia.Properties.Cluster
  798. if options.IdleByNode {
  799. key = fmt.Sprintf("%s/%s", ia.Properties.Cluster, ia.Properties.Node)
  800. }
  801. rt, ok := allocTotals[key]
  802. if !ok {
  803. log.Warningf("SummaryAllocation: AggregateBy: cannot handle undistributed idle for '%s'", key)
  804. continue
  805. }
  806. hasUndistributableCost := false
  807. if ia.CPUCost > 0.0 && rt.CPUCost == 0.0 {
  808. // There is idle CPU cost, but no allocated CPU cost, so that cost
  809. // is undistributable and must be inserted.
  810. hasUndistributableCost = true
  811. } else {
  812. // Cost was entirely distributed, so zero it out
  813. ia.CPUCost = 0.0
  814. }
  815. if ia.GPUCost > 0.0 && rt.GPUCost == 0.0 {
  816. // There is idle GPU cost, but no allocated GPU cost, so that cost
  817. // is undistributable and must be inserted.
  818. hasUndistributableCost = true
  819. } else {
  820. // Cost was entirely distributed, so zero it out
  821. ia.GPUCost = 0.0
  822. }
  823. if ia.RAMCost > 0.0 && rt.RAMCost == 0.0 {
  824. // There is idle CPU cost, but no allocated CPU cost, so that cost
  825. // is undistributable and must be inserted.
  826. hasUndistributableCost = true
  827. } else {
  828. // Cost was entirely distributed, so zero it out
  829. ia.RAMCost = 0.0
  830. }
  831. if hasUndistributableCost {
  832. ia.Name = fmt.Sprintf("%s/%s", key, IdleSuffix)
  833. resultSet.Insert(ia)
  834. }
  835. }
  836. // 14. Combine all idle allocations into a single idle allocation, unless
  837. // the option to keep idle split by cluster or node is enabled.
  838. if !options.SplitIdle {
  839. for _, ia := range resultSet.idleAllocations() {
  840. resultSet.Delete(ia.Name)
  841. ia.Name = IdleSuffix
  842. resultSet.Insert(ia)
  843. }
  844. }
  845. // Replace the existing set's data with the new, aggregated summary data
  846. sas.SummaryAllocations = resultSet.SummaryAllocations
  847. return nil
  848. }
  849. // Delete removes the allocation with the given name from the set
  850. func (sas *SummaryAllocationSet) Delete(name string) {
  851. if sas == nil {
  852. return
  853. }
  854. sas.Lock()
  855. defer sas.Unlock()
  856. delete(sas.externalKeys, name)
  857. delete(sas.idleKeys, name)
  858. delete(sas.SummaryAllocations, name)
  859. }
  860. // Each invokes the given function for each SummaryAllocation in the set
  861. func (sas *SummaryAllocationSet) Each(f func(string, *SummaryAllocation)) {
  862. if sas == nil {
  863. return
  864. }
  865. for k, a := range sas.SummaryAllocations {
  866. f(k, a)
  867. }
  868. }
  869. // IdleAllocations returns a map of the idle allocations in the AllocationSet.
  870. func (sas *SummaryAllocationSet) idleAllocations() map[string]*SummaryAllocation {
  871. idles := map[string]*SummaryAllocation{}
  872. if sas == nil || len(sas.SummaryAllocations) == 0 {
  873. return idles
  874. }
  875. sas.RLock()
  876. defer sas.RUnlock()
  877. for key := range sas.idleKeys {
  878. if sa, ok := sas.SummaryAllocations[key]; ok {
  879. idles[key] = sa
  880. }
  881. }
  882. return idles
  883. }
  884. // Insert aggregates the current entry in the SummaryAllocationSet by the given Allocation,
  885. // but only if the Allocation is valid, i.e. matches the SummaryAllocationSet's window. If
  886. // there is no existing entry, one is created. Nil error response indicates success.
  887. func (sas *SummaryAllocationSet) Insert(sa *SummaryAllocation) error {
  888. if sas == nil {
  889. return fmt.Errorf("cannot insert into nil SummaryAllocationSet")
  890. }
  891. if sa == nil {
  892. return fmt.Errorf("cannot insert a nil SummaryAllocation")
  893. }
  894. sas.Lock()
  895. defer sas.Unlock()
  896. if sas.SummaryAllocations == nil {
  897. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  898. }
  899. if sas.externalKeys == nil {
  900. sas.externalKeys = map[string]bool{}
  901. }
  902. if sas.idleKeys == nil {
  903. sas.idleKeys = map[string]bool{}
  904. }
  905. // Add the given Allocation to the existing entry, if there is one;
  906. // otherwise just set directly into allocations
  907. if _, ok := sas.SummaryAllocations[sa.Name]; ok {
  908. err := sas.SummaryAllocations[sa.Name].Add(sa)
  909. if err != nil {
  910. return fmt.Errorf("SummaryAllocationSet.Insert: error trying to Add: %s", err)
  911. }
  912. } else {
  913. sas.SummaryAllocations[sa.Name] = sa
  914. }
  915. // If the given Allocation is an external one, record that
  916. if sa.IsExternal() {
  917. sas.externalKeys[sa.Name] = true
  918. }
  919. // If the given Allocation is an idle one, record that
  920. if sa.IsIdle() {
  921. sas.idleKeys[sa.Name] = true
  922. }
  923. return nil
  924. }
  925. // SummaryAllocationSetRange is a thread-safe slice of SummaryAllocationSets.
  926. type SummaryAllocationSetRange struct {
  927. sync.RWMutex
  928. Step time.Duration `json:"step"`
  929. SummaryAllocationSets []*SummaryAllocationSet `json:"sets"`
  930. Window Window `json:"window"`
  931. }
  932. // NewSummaryAllocationSetRange instantiates a new range composed of the given
  933. // SummaryAllocationSets in the order provided. The expectations about the
  934. // SummaryAllocationSets are as follows:
  935. // - window durations are all equal
  936. // - sets are consecutive (i.e. chronologically sorted)
  937. // - there are no gaps between sets
  938. // - sets do not have overlapping windows
  939. func NewSummaryAllocationSetRange(sass ...*SummaryAllocationSet) *SummaryAllocationSetRange {
  940. var step time.Duration
  941. window := NewWindow(nil, nil)
  942. for _, sas := range sass {
  943. if window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*window.Start())) {
  944. window.start = sas.Window.Start()
  945. }
  946. if window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*window.End())) {
  947. window.end = sas.Window.End()
  948. }
  949. if step == 0 {
  950. step = sas.Window.Duration()
  951. } else if step != sas.Window.Duration() {
  952. log.Warningf("instantiating range with step %s using set of step %s is illegal", step, sas.Window.Duration())
  953. }
  954. }
  955. return &SummaryAllocationSetRange{
  956. Step: step,
  957. SummaryAllocationSets: sass,
  958. Window: window,
  959. }
  960. }
  961. // Accumulate sums each AllocationSet in the given range, returning a single cumulative
  962. // AllocationSet for the entire range.
  963. func (sasr *SummaryAllocationSetRange) Accumulate() (*SummaryAllocationSet, error) {
  964. var result *SummaryAllocationSet
  965. var err error
  966. sasr.RLock()
  967. defer sasr.RUnlock()
  968. for _, sas := range sasr.SummaryAllocationSets {
  969. result, err = result.Add(sas)
  970. if err != nil {
  971. return nil, err
  972. }
  973. }
  974. return result, nil
  975. }
  976. // AggregateBy aggregates each AllocationSet in the range by the given
  977. // properties and options.
  978. func (sasr *SummaryAllocationSetRange) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  979. sasr.Lock()
  980. defer sasr.Unlock()
  981. for _, sas := range sasr.SummaryAllocationSets {
  982. err := sas.AggregateBy(aggregateBy, options)
  983. if err != nil {
  984. // Wipe out data so that corrupt data cannot be mistakenly used
  985. sasr.SummaryAllocationSets = []*SummaryAllocationSet{}
  986. return err
  987. }
  988. }
  989. return nil
  990. }
  991. // Append appends the given AllocationSet to the end of the range. It does not
  992. // validate whether or not that violates window continuity.
  993. func (sasr *SummaryAllocationSetRange) Append(sas *SummaryAllocationSet) error {
  994. if sasr.Step != 0 && sas.Window.Duration() != sasr.Step {
  995. return fmt.Errorf("cannot append set with duration %s to range of step %s", sas.Window.Duration(), sasr.Step)
  996. }
  997. sasr.Lock()
  998. defer sasr.Unlock()
  999. // Append to list of sets
  1000. sasr.SummaryAllocationSets = append(sasr.SummaryAllocationSets, sas)
  1001. // Set step, if not set
  1002. if sasr.Step == 0 {
  1003. sasr.Step = sas.Window.Duration()
  1004. }
  1005. // Adjust window
  1006. if sasr.Window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*sasr.Window.Start())) {
  1007. sasr.Window.start = sas.Window.Start()
  1008. }
  1009. if sasr.Window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*sasr.Window.End())) {
  1010. sasr.Window.end = sas.Window.End()
  1011. }
  1012. return nil
  1013. }
  1014. // Each invokes the given function for each AllocationSet in the range
  1015. func (sasr *SummaryAllocationSetRange) Each(f func(int, *SummaryAllocationSet)) {
  1016. if sasr == nil {
  1017. return
  1018. }
  1019. for i, as := range sasr.SummaryAllocationSets {
  1020. f(i, as)
  1021. }
  1022. }
  1023. // InsertExternalAllocations takes all allocations in the given
  1024. // AllocationSetRange (they should all be considered "external") and inserts
  1025. // them into the receiving SummaryAllocationSetRange.
  1026. // TODO:CLEANUP replace this with a better idea (or get rid of external
  1027. // allocations, as such, altogether)
  1028. func (sasr *SummaryAllocationSetRange) InsertExternalAllocations(that *AllocationSetRange) error {
  1029. if sasr == nil {
  1030. return fmt.Errorf("cannot insert range into nil AllocationSetRange")
  1031. }
  1032. // keys maps window to index in range
  1033. keys := map[string]int{}
  1034. for i, as := range sasr.SummaryAllocationSets {
  1035. if as == nil {
  1036. continue
  1037. }
  1038. keys[as.Window.String()] = i
  1039. }
  1040. // Nothing to merge, so simply return
  1041. if len(keys) == 0 {
  1042. return nil
  1043. }
  1044. var err error
  1045. that.Each(func(j int, thatAS *AllocationSet) {
  1046. if thatAS == nil || err != nil {
  1047. return
  1048. }
  1049. // Find matching AllocationSet in asr
  1050. i, ok := keys[thatAS.Window.String()]
  1051. if !ok {
  1052. err = fmt.Errorf("cannot merge AllocationSet into window that does not exist: %s", thatAS.Window.String())
  1053. return
  1054. }
  1055. sas := sasr.SummaryAllocationSets[i]
  1056. // Insert each Allocation from the given set
  1057. thatAS.Each(func(k string, alloc *Allocation) {
  1058. externalSA := NewSummaryAllocation(alloc, true, true)
  1059. // This error will be returned below
  1060. // TODO:CLEANUP should Each have early-error-return functionality?
  1061. err = sas.Insert(externalSA)
  1062. })
  1063. })
  1064. // err might be nil
  1065. return err
  1066. }