summaryallocation.go 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373
  1. package kubecost
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "sync"
  7. "time"
  8. "github.com/opencost/opencost/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,
  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, keep 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, kfs []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(kfs) == 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 kept. If so, mark it as
  280. // such, insert it, and continue.
  281. shouldKeep := false
  282. for _, kf := range kfs {
  283. if kf(alloc) {
  284. shouldKeep = true
  285. break
  286. }
  287. }
  288. if shouldKeep {
  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. // Clone creates a deep copy of the SummaryAllocationSet
  320. func (sas *SummaryAllocationSet) Clone() *SummaryAllocationSet {
  321. sas.RLock()
  322. defer sas.RUnlock()
  323. externalKeys := make(map[string]bool, len(sas.externalKeys))
  324. for k, v := range sas.externalKeys {
  325. externalKeys[k] = v
  326. }
  327. idleKeys := make(map[string]bool, len(sas.idleKeys))
  328. for k, v := range sas.idleKeys {
  329. idleKeys[k] = v
  330. }
  331. summaryAllocations := make(map[string]*SummaryAllocation, len(sas.SummaryAllocations))
  332. for k, v := range sas.SummaryAllocations {
  333. summaryAllocations[k] = v.Clone()
  334. }
  335. return &SummaryAllocationSet{
  336. externalKeys: externalKeys,
  337. idleKeys: idleKeys,
  338. SummaryAllocations: summaryAllocations,
  339. Window: sas.Window.Clone(),
  340. }
  341. }
  342. // Add sums two SummaryAllocationSets, which Adds all SummaryAllocations in the
  343. // given SummaryAllocationSet to thier counterparts in the receiving set. Add
  344. // also expands the Window to include both constituent Windows, in the case
  345. // that Add is being used from accumulating (as opposed to aggregating). For
  346. // performance reasons, the function may return either a new set, or an
  347. // unmodified original, so it should not be assumed that the original sets are
  348. // safeuly usable after calling Add.
  349. func (sas *SummaryAllocationSet) Add(that *SummaryAllocationSet) (*SummaryAllocationSet, error) {
  350. if sas == nil || len(sas.SummaryAllocations) == 0 {
  351. return that, nil
  352. }
  353. if that == nil || len(that.SummaryAllocations) == 0 {
  354. return sas, nil
  355. }
  356. if sas.Window.IsOpen() {
  357. return nil, errors.New("cannot add a SummaryAllocationSet with an open window")
  358. }
  359. // Set start, end to min(start), max(end)
  360. start := *sas.Window.Start()
  361. end := *sas.Window.End()
  362. if that.Window.Start().Before(start) {
  363. start = *that.Window.Start()
  364. }
  365. if that.Window.End().After(end) {
  366. end = *that.Window.End()
  367. }
  368. acc := &SummaryAllocationSet{
  369. SummaryAllocations: make(map[string]*SummaryAllocation, len(sas.SummaryAllocations)),
  370. Window: NewClosedWindow(start, end),
  371. }
  372. sas.RLock()
  373. defer sas.RUnlock()
  374. that.RLock()
  375. defer that.RUnlock()
  376. for _, alloc := range sas.SummaryAllocations {
  377. err := acc.Insert(alloc)
  378. if err != nil {
  379. return nil, err
  380. }
  381. }
  382. for _, alloc := range that.SummaryAllocations {
  383. err := acc.Insert(alloc)
  384. if err != nil {
  385. return nil, err
  386. }
  387. }
  388. return acc, nil
  389. }
  390. // AggregateBy aggregates the Allocations in the given AllocationSet by the given
  391. // AllocationProperty. This will only be legal if the AllocationSet is divisible by the
  392. // given AllocationProperty; e.g. Containers can be divided by Namespace, but not vice-a-versa.
  393. func (sas *SummaryAllocationSet) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  394. if sas == nil || len(sas.SummaryAllocations) == 0 {
  395. return nil
  396. }
  397. if sas.Window.IsOpen() {
  398. return errors.New("cannot aggregate a SummaryAllocationSet with an open window")
  399. }
  400. if options == nil {
  401. options = &AllocationAggregationOptions{}
  402. }
  403. if options.LabelConfig == nil {
  404. options.LabelConfig = NewLabelConfig()
  405. }
  406. // Check if we have any work to do; if not, then early return. If
  407. // aggregateBy is nil, we don't aggregate anything. On the other hand,
  408. // an empty slice implies that we should aggregate everything. (See
  409. // generateKey for why that makes sense.)
  410. shouldAggregate := aggregateBy != nil
  411. shouldKeep := len(options.SharedHourlyCosts) > 0 || len(options.ShareFuncs) > 0
  412. if !shouldAggregate && !shouldKeep {
  413. return nil
  414. }
  415. // The order of operations for aggregating a SummaryAllotionSet is as
  416. // follows:
  417. //
  418. // 1. Partition external, idle, and shared allocations into separate sets.
  419. // Also, create the resultSet into which the results will be aggregated.
  420. //
  421. // 2. Record resource totals for shared costs and unmounted volumes so
  422. // that we can account for them in computing idle coefficients.
  423. //
  424. // 3. Retrieve pre-computed allocation resource totals, which will be used
  425. // to compute idle sharing coefficients.
  426. //
  427. // 4. Convert shared hourly cost into a cumulative allocation to share,
  428. // and insert it into the share set.
  429. //
  430. // 5. Compute sharing coefficients per-aggregation, if sharing resources.
  431. //
  432. // 6. Distribute idle allocations according to the idle coefficients.
  433. //
  434. // 7. Record allocation resource totals (after filtration) if filters have
  435. // been applied. (Used for filtering proportional amount of idle.)
  436. //
  437. // 8. Generate aggregation key and insert allocation into the output set
  438. //
  439. // 9. If idle is shared and resources are shared, it's probable that some
  440. // amount of idle cost will be shared with a shared resource.
  441. // Distribute that idle cost, if it exists, among the respective shared
  442. // allocations before sharing them with the aggregated allocations.
  443. //
  444. // 10. Apply idle filtration, which "filters" the idle cost, or scales it
  445. // by the proportion of allocation resources remaining after filters
  446. // have been applied.
  447. //
  448. // 11. Distribute shared resources according to sharing coefficients.
  449. //
  450. // 12. Insert external allocations into the result set.
  451. //
  452. // 13. Insert any undistributed idle, in the case that idle
  453. // coefficients end up being zero and some idle is not shared.
  454. //
  455. // 14. Combine all idle allocations into a single idle allocation, unless
  456. // the option to keep idle split by cluster or node is enabled.
  457. // 1. Partition external, idle, and shared allocations into separate sets.
  458. // Also, create the resultSet into which the results will be aggregated.
  459. // resultSet will collect the aggregated allocations
  460. resultSet := &SummaryAllocationSet{
  461. Window: sas.Window.Clone(),
  462. }
  463. // externalSet will collect external allocations
  464. externalSet := &SummaryAllocationSet{
  465. Window: sas.Window.Clone(),
  466. }
  467. // idleSet will be shared among resultSet after initial aggregation
  468. // is complete
  469. idleSet := &SummaryAllocationSet{
  470. Window: sas.Window.Clone(),
  471. }
  472. // shareSet will be shared among resultSet after initial aggregation
  473. // is complete
  474. shareSet := &SummaryAllocationSet{
  475. Window: sas.Window.Clone(),
  476. }
  477. sas.Lock()
  478. defer sas.Unlock()
  479. // 2. Record resource totals for shared costs, aggregating by cluster or by
  480. // node (depending on if idle is partitioned by cluster or node) so that we
  481. // can account for them in computing idle coefficients. Do the same for
  482. // unmounted volume costs, which only require a total cost.
  483. sharedResourceTotals := map[string]*AllocationTotals{}
  484. totalUnmountedCost := 0.0
  485. // 1 & 2. Identify set membership and aggregate aforementioned totals.
  486. for _, sa := range sas.SummaryAllocations {
  487. if sa.Share {
  488. var key string
  489. if options.IdleByNode {
  490. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  491. } else {
  492. key = sa.Properties.Cluster
  493. }
  494. if _, ok := sharedResourceTotals[key]; !ok {
  495. sharedResourceTotals[key] = &AllocationTotals{}
  496. }
  497. sharedResourceTotals[key].CPUCost += sa.CPUCost
  498. sharedResourceTotals[key].GPUCost += sa.GPUCost
  499. sharedResourceTotals[key].LoadBalancerCost += sa.LoadBalancerCost
  500. sharedResourceTotals[key].NetworkCost += sa.NetworkCost
  501. sharedResourceTotals[key].PersistentVolumeCost += sa.PVCost
  502. sharedResourceTotals[key].RAMCost += sa.RAMCost
  503. shareSet.Insert(sa)
  504. delete(sas.SummaryAllocations, sa.Name)
  505. continue
  506. }
  507. // External allocations get aggregated post-hoc (see step 6) and do
  508. // not necessarily contain complete sets of properties, so they are
  509. // moved to a separate AllocationSet.
  510. if sa.IsExternal() {
  511. delete(sas.externalKeys, sa.Name)
  512. delete(sas.SummaryAllocations, sa.Name)
  513. externalSet.Insert(sa)
  514. continue
  515. }
  516. // Idle allocations should be separated into idleSet if they are to be
  517. // shared later on. If they are not to be shared, then add them to the
  518. // resultSet like any other allocation.
  519. if sa.IsIdle() {
  520. delete(sas.idleKeys, sa.Name)
  521. delete(sas.SummaryAllocations, sa.Name)
  522. if options.ShareIdle == ShareEven || options.ShareIdle == ShareWeighted {
  523. idleSet.Insert(sa)
  524. } else {
  525. resultSet.Insert(sa)
  526. }
  527. continue
  528. }
  529. // Track total unmounted cost because it must be taken out of total
  530. // allocated costs for sharing coefficients.
  531. if sa.IsUnmounted() {
  532. totalUnmountedCost += sa.TotalCost()
  533. }
  534. }
  535. // It's possible that no more un-shared, non-idle, non-external allocations
  536. // remain at this point. This always results in an emptySet, so return early.
  537. if len(sas.SummaryAllocations) == 0 {
  538. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  539. return nil
  540. }
  541. // 3. Retrieve pre-computed allocation resource totals, which will be used
  542. // to compute idle coefficients, based on the ratio of an allocation's per-
  543. // resource cost to the per-resource totals of that allocation's cluster or
  544. // node. Whether to perform this operation based on cluster or node is an
  545. // option. (See IdleByNode documentation; defaults to idle-by-cluster.)
  546. var allocTotals map[string]*AllocationTotals
  547. var ok bool
  548. if options.AllocationTotalsStore != nil {
  549. if options.IdleByNode {
  550. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByNode(*sas.Window.Start(), *sas.Window.End())
  551. if !ok {
  552. return fmt.Errorf("nil allocation resource totals by node for %s", sas.Window)
  553. }
  554. } else {
  555. allocTotals, ok = options.AllocationTotalsStore.GetAllocationTotalsByCluster(*sas.Window.Start(), *sas.Window.End())
  556. if !ok {
  557. return fmt.Errorf("nil allocation resource totals by cluster for %s", sas.Window)
  558. }
  559. }
  560. }
  561. // If reconciliation has been fully or partially disabled, clear the
  562. // relevant adjustments from the alloc totals
  563. if allocTotals != nil && (!options.Reconcile || !options.ReconcileNetwork) {
  564. if !options.Reconcile {
  565. for _, tot := range allocTotals {
  566. tot.ClearAdjustments()
  567. }
  568. } else if !options.ReconcileNetwork {
  569. for _, tot := range allocTotals {
  570. tot.NetworkCostAdjustment = 0.0
  571. }
  572. }
  573. }
  574. // If filters have been applied, then we need to record allocation resource
  575. // totals after filtration (i.e. the allocations that are present) so that
  576. // we can identify the proportion of idle cost to keep. That is, we should
  577. // only return the idle cost that would be shared with the remaining
  578. // allocations, even if we're keeping idle separate. The totals should be
  579. // recorded by idle-key (cluster or node, depending on the IdleByNode
  580. // option). Instantiating this map is a signal to record the totals.
  581. var allocTotalsAfterFilters map[string]*AllocationTotals
  582. if len(resultSet.idleKeys) > 0 && len(options.FilterFuncs) > 0 {
  583. allocTotalsAfterFilters = make(map[string]*AllocationTotals, len(resultSet.idleKeys))
  584. }
  585. // If we're recording allocTotalsAfterFilters and there are shared costs,
  586. // then record those resource totals here so that idle for those shared
  587. // resources gets included.
  588. if allocTotalsAfterFilters != nil {
  589. for key, rt := range sharedResourceTotals {
  590. if _, ok := allocTotalsAfterFilters[key]; !ok {
  591. allocTotalsAfterFilters[key] = &AllocationTotals{}
  592. }
  593. // Record only those fields required for computing idle
  594. allocTotalsAfterFilters[key].CPUCost += rt.CPUCost
  595. allocTotalsAfterFilters[key].GPUCost += rt.GPUCost
  596. allocTotalsAfterFilters[key].RAMCost += rt.RAMCost
  597. }
  598. }
  599. // 4. Convert shared hourly cost into a cumulative allocation to share,
  600. // and insert it into the share set.
  601. for name, cost := range options.SharedHourlyCosts {
  602. if cost > 0.0 {
  603. hours := sas.Window.Hours()
  604. // If set ends in the future, adjust hours accordingly
  605. diff := time.Since(*sas.Window.End())
  606. if diff < 0.0 {
  607. hours += diff.Hours()
  608. }
  609. totalSharedCost := cost * hours
  610. shareSet.Insert(&SummaryAllocation{
  611. Name: fmt.Sprintf("%s/%s", name, SharedSuffix),
  612. Properties: &AllocationProperties{},
  613. Start: *sas.Window.Start(),
  614. End: *sas.Window.End(),
  615. SharedCost: totalSharedCost,
  616. })
  617. }
  618. }
  619. // Sharing coefficients are recorded by post-aggregation-key (e.g. if
  620. // aggregating by namespace, then the key will be the namespace) and only
  621. // need to be recorded if there are shared resources. Instantiating this
  622. // map is the signal to record sharing coefficients.
  623. var sharingCoeffs map[string]float64
  624. if len(shareSet.SummaryAllocations) > 0 {
  625. sharingCoeffs = map[string]float64{}
  626. }
  627. // Loop over all remaining SummaryAllocations (after filters, sharing, &c.)
  628. // doing the following, in this order:
  629. // 5. Compute sharing coefficients, if there are shared resources
  630. // 6. Distribute idle cost, if sharing idle
  631. // 7. Record allocTotalsAfterFiltration, if filters have been applied
  632. // 8. Aggregate by key
  633. for _, sa := range sas.SummaryAllocations {
  634. // Generate key to use for aggregation-by-key and allocation name
  635. key := sa.generateKey(aggregateBy, options.LabelConfig)
  636. // 5. Incrementally add to sharing coefficients before adding idle
  637. // cost, which would skew the coefficients. These coefficients will be
  638. // later divided by a total, turning them into a coefficient between
  639. // 0.0 and 1.0.
  640. // NOTE: SummaryAllocation does not support ShareEven, so only record
  641. // by cost for cost-weighted distribution.
  642. if sharingCoeffs != nil {
  643. sharingCoeffs[key] += sa.TotalCost() - sa.SharedCost
  644. }
  645. // 6. Distribute idle allocations according to the idle coefficients.
  646. // NOTE: if idle allocation is off (i.e. options.ShareIdle: ShareNone)
  647. // then all idle allocations will be in the resultSet at this point, so
  648. // idleSet will be empty and we won't enter this block.
  649. if len(idleSet.SummaryAllocations) > 0 {
  650. for _, idle := range idleSet.SummaryAllocations {
  651. // Idle key is either cluster or node, as determined by the
  652. // IdleByNode option.
  653. var key string
  654. // Only share idle allocation with current allocation (sa) if
  655. // the relevant properties match (i.e. cluster and/or node)
  656. if idle.Properties.Cluster != sa.Properties.Cluster {
  657. continue
  658. }
  659. key = idle.Properties.Cluster
  660. if options.IdleByNode {
  661. if idle.Properties.Node != sa.Properties.Node {
  662. continue
  663. }
  664. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  665. }
  666. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  667. sa.CPUCost += idle.CPUCost * cpuCoeff
  668. sa.GPUCost += idle.GPUCost * gpuCoeff
  669. sa.RAMCost += idle.RAMCost * ramCoeff
  670. }
  671. }
  672. // The key becomes the allocation's name, which is used as the key by
  673. // which the allocation is inserted into the set.
  674. sa.Name = key
  675. // If merging unallocated allocations, rename all unallocated
  676. // allocations as simply __unallocated__
  677. if options.MergeUnallocated && sa.IsUnallocated() {
  678. sa.Name = UnallocatedSuffix
  679. }
  680. // 7. Record filtered resource totals for idle allocation filtration,
  681. // only if necessary.
  682. if allocTotalsAfterFilters != nil {
  683. key := sa.Properties.Cluster
  684. if options.IdleByNode {
  685. key = fmt.Sprintf("%s/%s", sa.Properties.Cluster, sa.Properties.Node)
  686. }
  687. if _, ok := allocTotalsAfterFilters[key]; !ok {
  688. allocTotalsAfterFilters[key] = &AllocationTotals{}
  689. }
  690. allocTotalsAfterFilters[key].CPUCost += sa.CPUCost
  691. allocTotalsAfterFilters[key].GPUCost += sa.GPUCost
  692. allocTotalsAfterFilters[key].RAMCost += sa.RAMCost
  693. }
  694. // 8. Inserting the allocation with the generated key for a name
  695. // performs the actual aggregation step.
  696. resultSet.Insert(sa)
  697. }
  698. // 9. If idle is shared and resources are shared, it's probable that some
  699. // amount of idle cost will be shared with a shared resource. Distribute
  700. // that idle cost, if it exists, among the respective shared allocations
  701. // before sharing them with the aggregated allocations.
  702. if len(idleSet.SummaryAllocations) > 0 && len(shareSet.SummaryAllocations) > 0 {
  703. for _, sa := range shareSet.SummaryAllocations {
  704. for _, idle := range idleSet.SummaryAllocations {
  705. var key string
  706. // Only share idle allocation with current allocation (sa) if
  707. // the relevant property matches (i.e. Cluster or Node,
  708. // depending on which idle sharing option is selected)
  709. if options.IdleByNode {
  710. if idle.Properties.Cluster != sa.Properties.Cluster || idle.Properties.Node != sa.Properties.Node {
  711. continue
  712. }
  713. key = fmt.Sprintf("%s/%s", idle.Properties.Cluster, idle.Properties.Node)
  714. } else {
  715. if idle.Properties.Cluster != sa.Properties.Cluster {
  716. continue
  717. }
  718. key = idle.Properties.Cluster
  719. }
  720. cpuCoeff, gpuCoeff, ramCoeff := ComputeIdleCoefficients(options.ShareIdle, key, sa.CPUCost, sa.GPUCost, sa.RAMCost, allocTotals)
  721. sa.CPUCost += idle.CPUCost * cpuCoeff
  722. sa.GPUCost += idle.GPUCost * gpuCoeff
  723. sa.RAMCost += idle.RAMCost * ramCoeff
  724. }
  725. }
  726. }
  727. // 10. Apply idle filtration, which "filters" the idle cost, i.e. scales
  728. // idle allocation costs per-resource by the proportion of allocation
  729. // resources remaining after filtering. In effect, this returns only the
  730. // idle costs that would have been shared with the remaining allocations,
  731. // even if idle is kept separated.
  732. if allocTotalsAfterFilters != nil {
  733. for idleKey := range resultSet.idleKeys {
  734. ia := resultSet.SummaryAllocations[idleKey]
  735. var key string
  736. if options.IdleByNode {
  737. key = fmt.Sprintf("%s/%s", ia.Properties.Cluster, ia.Properties.Node)
  738. } else {
  739. key = ia.Properties.Cluster
  740. }
  741. // Percentage of idle that should remain after filters are applied,
  742. // which equals the proportion of filtered-to-actual cost.
  743. cpuFilterCoeff := 0.0
  744. if allocTotals[key].TotalCPUCost() > 0.0 {
  745. filteredAlloc, ok := allocTotalsAfterFilters[key]
  746. if ok {
  747. cpuFilterCoeff = filteredAlloc.TotalCPUCost() / allocTotals[key].TotalCPUCost()
  748. } else {
  749. cpuFilterCoeff = 0.0
  750. }
  751. }
  752. gpuFilterCoeff := 0.0
  753. if allocTotals[key].TotalGPUCost() > 0.0 {
  754. filteredAlloc, ok := allocTotalsAfterFilters[key]
  755. if ok {
  756. gpuFilterCoeff = filteredAlloc.TotalGPUCost() / allocTotals[key].TotalGPUCost()
  757. } else {
  758. gpuFilterCoeff = 0.0
  759. }
  760. }
  761. ramFilterCoeff := 0.0
  762. if allocTotals[key].TotalRAMCost() > 0.0 {
  763. filteredAlloc, ok := allocTotalsAfterFilters[key]
  764. if ok {
  765. ramFilterCoeff = filteredAlloc.TotalRAMCost() / allocTotals[key].TotalRAMCost()
  766. } else {
  767. ramFilterCoeff = 0.0
  768. }
  769. }
  770. ia.CPUCost *= cpuFilterCoeff
  771. ia.GPUCost *= gpuFilterCoeff
  772. ia.RAMCost *= ramFilterCoeff
  773. }
  774. }
  775. // 11. Distribute shared resources according to sharing coefficients.
  776. // NOTE: ShareEven is not supported
  777. if len(shareSet.SummaryAllocations) > 0 {
  778. sharingCoeffDenominator := 0.0
  779. for _, rt := range allocTotals {
  780. sharingCoeffDenominator += rt.TotalCost()
  781. }
  782. // Do not include the shared costs, themselves, when determining
  783. // sharing coefficients.
  784. for _, rt := range sharedResourceTotals {
  785. sharingCoeffDenominator -= rt.TotalCost()
  786. }
  787. // Do not include the unmounted costs when determining sharing
  788. // coefficients becuase they do not receive shared costs.
  789. sharingCoeffDenominator -= totalUnmountedCost
  790. if sharingCoeffDenominator <= 0.0 {
  791. log.Warnf("SummaryAllocation: sharing coefficient denominator is %f", sharingCoeffDenominator)
  792. } else {
  793. // Compute sharing coeffs by dividing the thus-far accumulated
  794. // numerators by the now-finalized denominator.
  795. for key := range sharingCoeffs {
  796. if sharingCoeffs[key] > 0.0 {
  797. sharingCoeffs[key] /= sharingCoeffDenominator
  798. } else {
  799. log.Warnf("SummaryAllocation: detected illegal sharing coefficient for %s: %v (setting to zero)", key, sharingCoeffs[key])
  800. sharingCoeffs[key] = 0.0
  801. }
  802. }
  803. for key, sa := range resultSet.SummaryAllocations {
  804. // Idle and unmounted allocations, by definition, do not
  805. // receive shared cost
  806. if sa.IsIdle() || sa.IsUnmounted() {
  807. continue
  808. }
  809. sharingCoeff := sharingCoeffs[key]
  810. // Distribute each shared cost with the current allocation on the
  811. // basis of the proportion of the allocation's cost (ShareWeighted)
  812. // or count (ShareEven) to the total aggregated cost or count. This
  813. // condition should hold in spite of filters because the sharing
  814. // coefficient denominator is held constant by pre-computed
  815. // resource totals and the post-aggregation total cost of the
  816. // remaining allocations will, by definition, not be affected.
  817. for _, shared := range shareSet.SummaryAllocations {
  818. sa.SharedCost += shared.TotalCost() * sharingCoeff
  819. }
  820. }
  821. }
  822. }
  823. // 12. Insert external allocations into the result set.
  824. for _, sa := range externalSet.SummaryAllocations {
  825. skip := false
  826. // Make an allocation with the same properties and test that
  827. // against the FilterFunc to see if the external allocation should
  828. // be filtered or not.
  829. // TODO:CLEANUP do something about external cost, this stinks
  830. ea := &Allocation{Properties: sa.Properties}
  831. for _, ff := range options.FilterFuncs {
  832. if !ff(ea) {
  833. skip = true
  834. break
  835. }
  836. }
  837. if !skip {
  838. key := sa.generateKey(aggregateBy, options.LabelConfig)
  839. sa.Name = key
  840. resultSet.Insert(sa)
  841. }
  842. }
  843. // 13. Distribute remaining, undistributed idle. Undistributed idle is any
  844. // per-resource idle cost for which there can be no idle coefficient
  845. // computed because there is zero usage across all allocations.
  846. for _, isa := range idleSet.SummaryAllocations {
  847. // if the idle does not apply to the non-filtered values, skip it
  848. skip := false
  849. // Make an allocation with the same properties and test that
  850. // against the FilterFunc to see if the external allocation should
  851. // be filtered or not.
  852. // TODO:CLEANUP do something about external cost, this stinks
  853. ia := &Allocation{Properties: isa.Properties}
  854. for _, ff := range options.FilterFuncs {
  855. if !ff(ia) {
  856. skip = true
  857. break
  858. }
  859. }
  860. if skip {
  861. continue
  862. }
  863. key := isa.Properties.Cluster
  864. if options.IdleByNode {
  865. key = fmt.Sprintf("%s/%s", isa.Properties.Cluster, isa.Properties.Node)
  866. }
  867. rt, ok := allocTotals[key]
  868. if !ok {
  869. log.Warnf("SummaryAllocation: AggregateBy: cannot handle undistributed idle for '%s'", key)
  870. continue
  871. }
  872. hasUndistributableCost := false
  873. if isa.CPUCost > 0.0 && rt.CPUCost == 0.0 {
  874. // There is idle CPU cost, but no allocated CPU cost, so that cost
  875. // is undistributable and must be inserted.
  876. hasUndistributableCost = true
  877. } else {
  878. // Cost was entirely distributed, so zero it out
  879. isa.CPUCost = 0.0
  880. }
  881. if isa.GPUCost > 0.0 && rt.GPUCost == 0.0 {
  882. // There is idle GPU cost, but no allocated GPU cost, so that cost
  883. // is undistributable and must be inserted.
  884. hasUndistributableCost = true
  885. } else {
  886. // Cost was entirely distributed, so zero it out
  887. isa.GPUCost = 0.0
  888. }
  889. if isa.RAMCost > 0.0 && rt.RAMCost == 0.0 {
  890. // There is idle CPU cost, but no allocated CPU cost, so that cost
  891. // is undistributable and must be inserted.
  892. hasUndistributableCost = true
  893. } else {
  894. // Cost was entirely distributed, so zero it out
  895. isa.RAMCost = 0.0
  896. }
  897. if hasUndistributableCost {
  898. isa.Name = fmt.Sprintf("%s/%s", key, IdleSuffix)
  899. resultSet.Insert(isa)
  900. }
  901. }
  902. // 14. Combine all idle allocations into a single idle allocation, unless
  903. // the option to keep idle split by cluster or node is enabled.
  904. if !options.SplitIdle {
  905. for _, ia := range resultSet.idleAllocations() {
  906. resultSet.Delete(ia.Name)
  907. ia.Name = IdleSuffix
  908. resultSet.Insert(ia)
  909. }
  910. }
  911. // Replace the existing set's data with the new, aggregated summary data
  912. sas.SummaryAllocations = resultSet.SummaryAllocations
  913. return nil
  914. }
  915. // Delete removes the allocation with the given name from the set
  916. func (sas *SummaryAllocationSet) Delete(name string) {
  917. if sas == nil {
  918. return
  919. }
  920. sas.Lock()
  921. defer sas.Unlock()
  922. delete(sas.externalKeys, name)
  923. delete(sas.idleKeys, name)
  924. delete(sas.SummaryAllocations, name)
  925. }
  926. // Each invokes the given function for each SummaryAllocation in the set
  927. func (sas *SummaryAllocationSet) Each(f func(string, *SummaryAllocation)) {
  928. if sas == nil {
  929. return
  930. }
  931. for k, a := range sas.SummaryAllocations {
  932. f(k, a)
  933. }
  934. }
  935. // IdleAllocations returns a map of the idle allocations in the AllocationSet.
  936. func (sas *SummaryAllocationSet) idleAllocations() map[string]*SummaryAllocation {
  937. idles := map[string]*SummaryAllocation{}
  938. if sas == nil || len(sas.SummaryAllocations) == 0 {
  939. return idles
  940. }
  941. sas.RLock()
  942. defer sas.RUnlock()
  943. for key := range sas.idleKeys {
  944. if sa, ok := sas.SummaryAllocations[key]; ok {
  945. idles[key] = sa
  946. }
  947. }
  948. return idles
  949. }
  950. // Insert aggregates the current entry in the SummaryAllocationSet by the given Allocation,
  951. // but only if the Allocation is valid, i.e. matches the SummaryAllocationSet's window. If
  952. // there is no existing entry, one is created. Nil error response indicates success.
  953. func (sas *SummaryAllocationSet) Insert(sa *SummaryAllocation) error {
  954. if sas == nil {
  955. return fmt.Errorf("cannot insert into nil SummaryAllocationSet")
  956. }
  957. if sa == nil {
  958. return fmt.Errorf("cannot insert a nil SummaryAllocation")
  959. }
  960. sas.Lock()
  961. defer sas.Unlock()
  962. if sas.SummaryAllocations == nil {
  963. sas.SummaryAllocations = map[string]*SummaryAllocation{}
  964. }
  965. if sas.externalKeys == nil {
  966. sas.externalKeys = map[string]bool{}
  967. }
  968. if sas.idleKeys == nil {
  969. sas.idleKeys = map[string]bool{}
  970. }
  971. // Add the given Allocation to the existing entry, if there is one;
  972. // otherwise just set directly into allocations
  973. if _, ok := sas.SummaryAllocations[sa.Name]; ok {
  974. err := sas.SummaryAllocations[sa.Name].Add(sa)
  975. if err != nil {
  976. return fmt.Errorf("SummaryAllocationSet.Insert: error trying to Add: %s", err)
  977. }
  978. } else {
  979. sas.SummaryAllocations[sa.Name] = sa
  980. }
  981. // If the given Allocation is an external one, record that
  982. if sa.IsExternal() {
  983. sas.externalKeys[sa.Name] = true
  984. }
  985. // If the given Allocation is an idle one, record that
  986. if sa.IsIdle() {
  987. sas.idleKeys[sa.Name] = true
  988. }
  989. return nil
  990. }
  991. func (sas *SummaryAllocationSet) TotalCost() float64 {
  992. if sas == nil {
  993. return 0.0
  994. }
  995. sas.RLock()
  996. defer sas.RUnlock()
  997. tc := 0.0
  998. for _, sa := range sas.SummaryAllocations {
  999. tc += sa.TotalCost()
  1000. }
  1001. return tc
  1002. }
  1003. // SummaryAllocationSetRange is a thread-safe slice of SummaryAllocationSets.
  1004. type SummaryAllocationSetRange struct {
  1005. sync.RWMutex
  1006. Step time.Duration `json:"step"`
  1007. SummaryAllocationSets []*SummaryAllocationSet `json:"sets"`
  1008. Window Window `json:"window"`
  1009. Message string `json:"-"`
  1010. }
  1011. // NewSummaryAllocationSetRange instantiates a new range composed of the given
  1012. // SummaryAllocationSets in the order provided. The expectations about the
  1013. // SummaryAllocationSets are as follows:
  1014. // - window durations are all equal
  1015. // - sets are consecutive (i.e. chronologically sorted)
  1016. // - there are no gaps between sets
  1017. // - sets do not have overlapping windows
  1018. func NewSummaryAllocationSetRange(sass ...*SummaryAllocationSet) *SummaryAllocationSetRange {
  1019. var step time.Duration
  1020. window := NewWindow(nil, nil)
  1021. for _, sas := range sass {
  1022. if window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*window.Start())) {
  1023. window.start = sas.Window.Start()
  1024. }
  1025. if window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*window.End())) {
  1026. window.end = sas.Window.End()
  1027. }
  1028. if step == 0 {
  1029. step = sas.Window.Duration()
  1030. } else if step != sas.Window.Duration() {
  1031. log.Warnf("instantiating range with step %s using set of step %s is illegal", step, sas.Window.Duration())
  1032. }
  1033. }
  1034. return &SummaryAllocationSetRange{
  1035. Step: step,
  1036. SummaryAllocationSets: sass,
  1037. Window: window,
  1038. }
  1039. }
  1040. // Accumulate sums each AllocationSet in the given range, returning a single cumulative
  1041. // AllocationSet for the entire range.
  1042. func (sasr *SummaryAllocationSetRange) Accumulate() (*SummaryAllocationSet, error) {
  1043. var result *SummaryAllocationSet
  1044. var err error
  1045. sasr.RLock()
  1046. defer sasr.RUnlock()
  1047. for _, sas := range sasr.SummaryAllocationSets {
  1048. result, err = result.Add(sas)
  1049. if err != nil {
  1050. return nil, err
  1051. }
  1052. }
  1053. return result, nil
  1054. }
  1055. // AggregateBy aggregates each AllocationSet in the range by the given
  1056. // properties and options.
  1057. func (sasr *SummaryAllocationSetRange) AggregateBy(aggregateBy []string, options *AllocationAggregationOptions) error {
  1058. sasr.Lock()
  1059. defer sasr.Unlock()
  1060. for _, sas := range sasr.SummaryAllocationSets {
  1061. err := sas.AggregateBy(aggregateBy, options)
  1062. if err != nil {
  1063. // Wipe out data so that corrupt data cannot be mistakenly used
  1064. sasr.SummaryAllocationSets = []*SummaryAllocationSet{}
  1065. return err
  1066. }
  1067. }
  1068. return nil
  1069. }
  1070. // Append appends the given AllocationSet to the end of the range. It does not
  1071. // validate whether or not that violates window continuity.
  1072. func (sasr *SummaryAllocationSetRange) Append(sas *SummaryAllocationSet) error {
  1073. if sasr.Step != 0 && sas.Window.Duration() != sasr.Step {
  1074. return fmt.Errorf("cannot append set with duration %s to range of step %s", sas.Window.Duration(), sasr.Step)
  1075. }
  1076. sasr.Lock()
  1077. defer sasr.Unlock()
  1078. // Append to list of sets
  1079. sasr.SummaryAllocationSets = append(sasr.SummaryAllocationSets, sas)
  1080. // Set step, if not set
  1081. if sasr.Step == 0 {
  1082. sasr.Step = sas.Window.Duration()
  1083. }
  1084. // Adjust window
  1085. if sasr.Window.Start() == nil || (sas.Window.Start() != nil && sas.Window.Start().Before(*sasr.Window.Start())) {
  1086. sasr.Window.start = sas.Window.Start()
  1087. }
  1088. if sasr.Window.End() == nil || (sas.Window.End() != nil && sas.Window.End().After(*sasr.Window.End())) {
  1089. sasr.Window.end = sas.Window.End()
  1090. }
  1091. return nil
  1092. }
  1093. // Each invokes the given function for each AllocationSet in the range
  1094. func (sasr *SummaryAllocationSetRange) Each(f func(int, *SummaryAllocationSet)) {
  1095. if sasr == nil {
  1096. return
  1097. }
  1098. for i, as := range sasr.SummaryAllocationSets {
  1099. f(i, as)
  1100. }
  1101. }
  1102. // InsertExternalAllocations takes all allocations in the given
  1103. // AllocationSetRange (they should all be considered "external") and inserts
  1104. // them into the receiving SummaryAllocationSetRange.
  1105. // TODO:CLEANUP replace this with a better idea (or get rid of external
  1106. // allocations, as such, altogether)
  1107. func (sasr *SummaryAllocationSetRange) InsertExternalAllocations(that *AllocationSetRange) error {
  1108. if sasr == nil {
  1109. return fmt.Errorf("cannot insert range into nil AllocationSetRange")
  1110. }
  1111. // keys maps window to index in range
  1112. keys := map[string]int{}
  1113. for i, as := range sasr.SummaryAllocationSets {
  1114. if as == nil {
  1115. continue
  1116. }
  1117. keys[as.Window.String()] = i
  1118. }
  1119. // Nothing to merge, so simply return
  1120. if len(keys) == 0 {
  1121. return nil
  1122. }
  1123. var err error
  1124. that.Each(func(j int, thatAS *AllocationSet) {
  1125. if thatAS == nil || err != nil {
  1126. return
  1127. }
  1128. // Find matching AllocationSet in asr
  1129. i, ok := keys[thatAS.Window.String()]
  1130. if !ok {
  1131. err = fmt.Errorf("cannot merge AllocationSet into window that does not exist: %s", thatAS.Window.String())
  1132. return
  1133. }
  1134. sas := sasr.SummaryAllocationSets[i]
  1135. // Insert each Allocation from the given set
  1136. thatAS.Each(func(k string, alloc *Allocation) {
  1137. externalSA := NewSummaryAllocation(alloc, true, true)
  1138. // This error will be returned below
  1139. // TODO:CLEANUP should Each have early-error-return functionality?
  1140. err = sas.Insert(externalSA)
  1141. })
  1142. })
  1143. // err might be nil
  1144. return err
  1145. }
  1146. func (sasr *SummaryAllocationSetRange) TotalCost() float64 {
  1147. if sasr == nil {
  1148. return 0.0
  1149. }
  1150. sasr.RLock()
  1151. defer sasr.RUnlock()
  1152. tc := 0.0
  1153. for _, sas := range sasr.SummaryAllocationSets {
  1154. tc += sas.TotalCost()
  1155. }
  1156. return tc
  1157. }
  1158. // TODO remove after testing
  1159. func (sasr *SummaryAllocationSetRange) Print(verbose bool) {
  1160. fmt.Printf("%s (dur=%s, len=%d, cost=%.5f)\n", sasr.Window, sasr.Window.Duration(), len(sasr.SummaryAllocationSets), sasr.TotalCost())
  1161. for _, sas := range sasr.SummaryAllocationSets {
  1162. fmt.Printf(" > %s (dur=%s, len=%d, cost=%.5f) \n", sas.Window, sas.Window.Duration(), len(sas.SummaryAllocations), sas.TotalCost())
  1163. for key, sa := range sas.SummaryAllocations {
  1164. if verbose {
  1165. fmt.Printf(" {\"%s\", cpu: %.5f, gpu: %.5f, lb: %.5f, net: %.5f, pv: %.5f, ram: %.5f, shared: %.5f, external: %.5f}\n",
  1166. key, sa.CPUCost, sa.GPUCost, sa.LoadBalancerCost, sa.NetworkCost, sa.PVCost, sa.RAMCost, sa.SharedCost, sa.ExternalCost)
  1167. } else {
  1168. fmt.Printf(" - \"%s\": %.5f\n", key, sa.TotalCost())
  1169. }
  1170. }
  1171. }
  1172. }