totals.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785
  1. package opencost
  2. import (
  3. "errors"
  4. "fmt"
  5. "strconv"
  6. "time"
  7. "github.com/opencost/opencost/core/pkg/log"
  8. "github.com/patrickmn/go-cache"
  9. )
  10. type AllocationTotalsResult struct {
  11. Cluster map[string]*AllocationTotals `json:"cluster"`
  12. Node map[string]*AllocationTotals `json:"node"`
  13. }
  14. type AssetTotalsResult struct {
  15. Cluster map[string]*AssetTotals `json:"cluster"`
  16. Node map[string]*AssetTotals `json:"node"`
  17. }
  18. // AllocationTotals represents aggregate costs of all Allocations for
  19. // a given cluster or tuple of (cluster, node) between a given start and end
  20. // time, where the costs are aggregated per-resource. AllocationTotals
  21. // is designed to be used as a pre-computed intermediate data structure when
  22. // contextual knowledge is required to carry out a task, but computing totals
  23. // on-the-fly would be expensive; e.g. idle allocation; sharing coefficients
  24. // for idle or shared resources, etc.
  25. type AllocationTotals struct {
  26. Start time.Time `json:"start"`
  27. End time.Time `json:"end"`
  28. Cluster string `json:"cluster"`
  29. Node string `json:"node"`
  30. Count int `json:"count"`
  31. CPUCost float64 `json:"cpuCost"`
  32. CPUCostAdjustment float64 `json:"cpuCostAdjustment"`
  33. GPUCost float64 `json:"gpuCost"`
  34. GPUCostAdjustment float64 `json:"gpuCostAdjustment"`
  35. LoadBalancerCost float64 `json:"loadBalancerCost"`
  36. LoadBalancerCostAdjustment float64 `json:"loadBalancerCostAdjustment"`
  37. NetworkCost float64 `json:"networkCost"`
  38. NetworkCostAdjustment float64 `json:"networkCostAdjustment"`
  39. PersistentVolumeCost float64 `json:"persistentVolumeCost"`
  40. PersistentVolumeCostAdjustment float64 `json:"persistentVolumeCostAdjustment"`
  41. RAMCost float64 `json:"ramCost"`
  42. RAMCostAdjustment float64 `json:"ramCostAdjustment"`
  43. // UnmountedPVCost is used to track how much of the cost in
  44. // PersistentVolumeCost is for an unmounted PV. It is not additive of that
  45. // field, and need not be sent in API responses.
  46. UnmountedPVCost float64 `json:"-"`
  47. }
  48. // ClearAdjustments sets all adjustment fields to 0.0
  49. func (art *AllocationTotals) ClearAdjustments() {
  50. art.CPUCostAdjustment = 0.0
  51. art.GPUCostAdjustment = 0.0
  52. art.LoadBalancerCostAdjustment = 0.0
  53. art.NetworkCostAdjustment = 0.0
  54. art.PersistentVolumeCostAdjustment = 0.0
  55. art.RAMCostAdjustment = 0.0
  56. }
  57. // Clone deep copies the AllocationTotals
  58. func (art *AllocationTotals) Clone() *AllocationTotals {
  59. return &AllocationTotals{
  60. Start: art.Start,
  61. End: art.End,
  62. Cluster: art.Cluster,
  63. Node: art.Node,
  64. Count: art.Count,
  65. CPUCost: art.CPUCost,
  66. CPUCostAdjustment: art.CPUCostAdjustment,
  67. GPUCost: art.GPUCost,
  68. GPUCostAdjustment: art.GPUCostAdjustment,
  69. LoadBalancerCost: art.LoadBalancerCost,
  70. LoadBalancerCostAdjustment: art.LoadBalancerCostAdjustment,
  71. NetworkCost: art.NetworkCost,
  72. NetworkCostAdjustment: art.NetworkCostAdjustment,
  73. PersistentVolumeCost: art.PersistentVolumeCost,
  74. PersistentVolumeCostAdjustment: art.PersistentVolumeCostAdjustment,
  75. RAMCost: art.RAMCost,
  76. RAMCostAdjustment: art.RAMCostAdjustment,
  77. }
  78. }
  79. // TotalCPUCost returns CPU cost with adjustment.
  80. func (art *AllocationTotals) TotalCPUCost() float64 {
  81. return art.CPUCost + art.CPUCostAdjustment
  82. }
  83. // TotalGPUCost returns GPU cost with adjustment.
  84. func (art *AllocationTotals) TotalGPUCost() float64 {
  85. return art.GPUCost + art.GPUCostAdjustment
  86. }
  87. // TotalLoadBalancerCost returns LoadBalancer cost with adjustment.
  88. func (art *AllocationTotals) TotalLoadBalancerCost() float64 {
  89. return art.LoadBalancerCost + art.LoadBalancerCostAdjustment
  90. }
  91. // TotalNetworkCost returns Network cost with adjustment.
  92. func (art *AllocationTotals) TotalNetworkCost() float64 {
  93. return art.NetworkCost + art.NetworkCostAdjustment
  94. }
  95. // TotalPersistentVolumeCost returns PersistentVolume cost with adjustment.
  96. func (art *AllocationTotals) TotalPersistentVolumeCost() float64 {
  97. return art.PersistentVolumeCost + art.PersistentVolumeCostAdjustment
  98. }
  99. // TotalRAMCost returns RAM cost with adjustment.
  100. func (art *AllocationTotals) TotalRAMCost() float64 {
  101. return art.RAMCost + art.RAMCostAdjustment
  102. }
  103. // TotalCost returns the sum of all costs.
  104. func (art *AllocationTotals) TotalCost() float64 {
  105. return art.TotalCPUCost() + art.TotalGPUCost() + art.TotalLoadBalancerCost() +
  106. art.TotalNetworkCost() + art.TotalPersistentVolumeCost() + art.TotalRAMCost()
  107. }
  108. // ComputeAllocationTotals totals the resource costs of the given AllocationSet
  109. // using the given property, i.e. cluster or node, where "node" really means to
  110. // use the fully-qualified (cluster, node) tuple.
  111. func ComputeAllocationTotals(as *AllocationSet, prop string) map[string]*AllocationTotals {
  112. arts := map[string]*AllocationTotals{}
  113. for _, alloc := range as.Allocations {
  114. // Do not count idle or unmounted allocations
  115. if alloc.IsIdle() || alloc.IsUnmounted() {
  116. continue
  117. }
  118. // Default to computing totals by Cluster, but allow override to use Node.
  119. key := alloc.Properties.Cluster
  120. if prop == AllocationNodeProp {
  121. key = fmt.Sprintf("%s/%s", alloc.Properties.Cluster, alloc.Properties.Node)
  122. }
  123. if _, ok := arts[key]; !ok {
  124. arts[key] = &AllocationTotals{
  125. Start: alloc.Start,
  126. End: alloc.End,
  127. Cluster: alloc.Properties.Cluster,
  128. Node: alloc.Properties.Node,
  129. }
  130. }
  131. if arts[key].Start.After(alloc.Start) {
  132. arts[key].Start = alloc.Start
  133. }
  134. if arts[key].End.Before(alloc.End) {
  135. arts[key].End = alloc.End
  136. }
  137. if arts[key].Node != alloc.Properties.Node {
  138. arts[key].Node = ""
  139. }
  140. arts[key].Count++
  141. arts[key].CPUCost += alloc.CPUCost
  142. arts[key].CPUCostAdjustment += alloc.CPUCostAdjustment
  143. arts[key].GPUCost += alloc.GPUCost
  144. arts[key].GPUCostAdjustment += alloc.GPUCostAdjustment
  145. arts[key].LoadBalancerCost += alloc.LoadBalancerCost
  146. arts[key].LoadBalancerCostAdjustment += alloc.LoadBalancerCostAdjustment
  147. arts[key].NetworkCost += alloc.NetworkCost
  148. arts[key].NetworkCostAdjustment += alloc.NetworkCostAdjustment
  149. arts[key].PersistentVolumeCost += alloc.PVCost() // NOTE: PVCost() does not include adjustment
  150. arts[key].PersistentVolumeCostAdjustment += alloc.PVCostAdjustment
  151. arts[key].RAMCost += alloc.RAMCost
  152. arts[key].RAMCostAdjustment += alloc.RAMCostAdjustment
  153. }
  154. return arts
  155. }
  156. // AllocationTotalsSet represents totals, summed by both "cluster" and "node"
  157. // for a given window of time.
  158. type AllocationTotalsSet struct {
  159. Cluster map[string]*AllocationTotals `json:"cluster"`
  160. Node map[string]*AllocationTotals `json:"node"`
  161. Window Window `json:"window"`
  162. }
  163. func NewAllocationTotalsSet(window Window, byCluster, byNode map[string]*AllocationTotals) *AllocationTotalsSet {
  164. return &AllocationTotalsSet{
  165. Cluster: byCluster,
  166. Node: byNode,
  167. Window: window.Clone(),
  168. }
  169. }
  170. // AssetTotals represents aggregate costs of all Assets for a given
  171. // cluster or tuple of (cluster, node) between a given start and end time,
  172. // where the costs are aggregated per-resource. AssetTotals is designed
  173. // to be used as a pre-computed intermediate data structure when contextual
  174. // knowledge is required to carry out a task, but computing totals on-the-fly
  175. // would be expensive; e.g. idle allocation, shared tenancy costs
  176. type AssetTotals struct {
  177. Start time.Time `json:"start"`
  178. End time.Time `json:"end"`
  179. Cluster string `json:"cluster"`
  180. Node string `json:"node"`
  181. Count int `json:"count"`
  182. AttachedVolumeCost float64 `json:"attachedVolumeCost"`
  183. AttachedVolumeCostAdjustment float64 `json:"attachedVolumeCostAdjustment"`
  184. ClusterManagementCost float64 `json:"clusterManagementCost"`
  185. ClusterManagementCostAdjustment float64 `json:"clusterManagementCostAdjustment"`
  186. CPUCost float64 `json:"cpuCost"`
  187. CPUCostAdjustment float64 `json:"cpuCostAdjustment"`
  188. GPUCost float64 `json:"gpuCost"`
  189. GPUCostAdjustment float64 `json:"gpuCostAdjustment"`
  190. LoadBalancerCost float64 `json:"loadBalancerCost"`
  191. LoadBalancerCostAdjustment float64 `json:"loadBalancerCostAdjustment"`
  192. PersistentVolumeCost float64 `json:"persistentVolumeCost"`
  193. PersistentVolumeCostAdjustment float64 `json:"persistentVolumeCostAdjustment"`
  194. RAMCost float64 `json:"ramCost"`
  195. RAMCostAdjustment float64 `json:"ramCostAdjustment"`
  196. PrivateLoadBalancer bool `json:"privateLoadBalancer"`
  197. }
  198. // ClearAdjustments sets all adjustment fields to 0.0
  199. func (art *AssetTotals) ClearAdjustments() {
  200. art.AttachedVolumeCostAdjustment = 0.0
  201. art.ClusterManagementCostAdjustment = 0.0
  202. art.CPUCostAdjustment = 0.0
  203. art.GPUCostAdjustment = 0.0
  204. art.LoadBalancerCostAdjustment = 0.0
  205. art.PersistentVolumeCostAdjustment = 0.0
  206. art.RAMCostAdjustment = 0.0
  207. }
  208. // Clone deep copies the AssetTotals
  209. func (art *AssetTotals) Clone() *AssetTotals {
  210. return &AssetTotals{
  211. Start: art.Start,
  212. End: art.End,
  213. Cluster: art.Cluster,
  214. Node: art.Node,
  215. Count: art.Count,
  216. AttachedVolumeCost: art.AttachedVolumeCost,
  217. AttachedVolumeCostAdjustment: art.AttachedVolumeCostAdjustment,
  218. ClusterManagementCost: art.ClusterManagementCost,
  219. ClusterManagementCostAdjustment: art.ClusterManagementCostAdjustment,
  220. CPUCost: art.CPUCost,
  221. CPUCostAdjustment: art.CPUCostAdjustment,
  222. GPUCost: art.GPUCost,
  223. GPUCostAdjustment: art.GPUCostAdjustment,
  224. LoadBalancerCost: art.LoadBalancerCost,
  225. LoadBalancerCostAdjustment: art.LoadBalancerCostAdjustment,
  226. PersistentVolumeCost: art.PersistentVolumeCost,
  227. PersistentVolumeCostAdjustment: art.PersistentVolumeCostAdjustment,
  228. RAMCost: art.RAMCost,
  229. RAMCostAdjustment: art.RAMCostAdjustment,
  230. PrivateLoadBalancer: art.PrivateLoadBalancer,
  231. }
  232. }
  233. // TotalAttachedVolumeCost returns CPU cost with adjustment.
  234. func (art *AssetTotals) TotalAttachedVolumeCost() float64 {
  235. return art.AttachedVolumeCost + art.AttachedVolumeCostAdjustment
  236. }
  237. // TotalClusterManagementCost returns ClusterManagement cost with adjustment.
  238. func (art *AssetTotals) TotalClusterManagementCost() float64 {
  239. return art.ClusterManagementCost + art.ClusterManagementCostAdjustment
  240. }
  241. // TotalCPUCost returns CPU cost with adjustment.
  242. func (art *AssetTotals) TotalCPUCost() float64 {
  243. return art.CPUCost + art.CPUCostAdjustment
  244. }
  245. // TotalGPUCost returns GPU cost with adjustment.
  246. func (art *AssetTotals) TotalGPUCost() float64 {
  247. return art.GPUCost + art.GPUCostAdjustment
  248. }
  249. // TotalLoadBalancerCost returns LoadBalancer cost with adjustment.
  250. func (art *AssetTotals) TotalLoadBalancerCost() float64 {
  251. return art.LoadBalancerCost + art.LoadBalancerCostAdjustment
  252. }
  253. // TotalPersistentVolumeCost returns PersistentVolume cost with adjustment.
  254. func (art *AssetTotals) TotalPersistentVolumeCost() float64 {
  255. return art.PersistentVolumeCost + art.PersistentVolumeCostAdjustment
  256. }
  257. // TotalRAMCost returns RAM cost with adjustment.
  258. func (art *AssetTotals) TotalRAMCost() float64 {
  259. return art.RAMCost + art.RAMCostAdjustment
  260. }
  261. // TotalCost returns the sum of all costs
  262. func (art *AssetTotals) TotalCost() float64 {
  263. return art.TotalAttachedVolumeCost() + art.TotalClusterManagementCost() +
  264. art.TotalCPUCost() + art.TotalGPUCost() + art.TotalLoadBalancerCost() +
  265. art.TotalPersistentVolumeCost() + art.TotalRAMCost()
  266. }
  267. // ComputeAssetTotals totals the resource costs of the given AssetSet,
  268. // using the given property, i.e. cluster or node, where "node" really means to
  269. // use the fully-qualified (cluster, node) tuple.
  270. // NOTE: we're not capturing LoadBalancers here yet, but only because we don't
  271. // yet need them. They could be added.
  272. func ComputeAssetTotals(as *AssetSet, byAsset bool) map[string]*AssetTotals {
  273. arts := map[string]*AssetTotals{}
  274. // Attached disks are tracked by matching their name with the name of the
  275. // node, as is standard for attached disks.
  276. nodeNames := map[string]bool{}
  277. disks := map[string]*Disk{}
  278. for _, node := range as.Nodes {
  279. // Default to computing totals by Cluster, but allow override to use Node.
  280. key := node.Properties.Cluster
  281. if byAsset {
  282. key = fmt.Sprintf("%s/%s", node.Properties.Cluster, node.Properties.Name)
  283. }
  284. // Add node name to list of node names. (These are to be used later
  285. // for attached volumes.)
  286. nodeNames[fmt.Sprintf("%s/%s", node.Properties.Cluster, node.Properties.Name)] = true
  287. // adjustmentRate is used to scale resource costs proportionally
  288. // by the adjustment. This is necessary because we only get one
  289. // adjustment per Node, not one per-resource-per-Node.
  290. //
  291. // e.g. total cost = $90 (cost = $100, adjustment = -$10) => 0.9000 ( 90 / 100)
  292. // e.g. total cost = $150 (cost = $450, adjustment = -$300) => 0.3333 (150 / 450)
  293. // e.g. total cost = $150 (cost = $100, adjustment = $50) => 1.5000 (150 / 100)
  294. adjustmentRate := 1.0
  295. if node.TotalCost()-node.Adjustment == 0 {
  296. // If (totalCost - adjustment) is 0.0 then adjustment cancels
  297. // the entire node cost and we should make everything 0
  298. // without dividing by 0.
  299. adjustmentRate = 0.0
  300. log.DedupedWarningf(5, "ComputeTotals: node cost adjusted to $0.00 for %s", node.Properties.Name)
  301. } else if node.Adjustment != 0.0 {
  302. // adjustmentRate is the ratio of cost-with-adjustment (i.e. TotalCost)
  303. // to cost-without-adjustment (i.e. TotalCost - Adjustment).
  304. adjustmentRate = node.TotalCost() / (node.TotalCost() - node.Adjustment)
  305. }
  306. // 1. Start with raw, measured resource cost
  307. // 2. Apply discount to get discounted resource cost
  308. // 3. Apply adjustment to get final "adjusted" resource cost
  309. // 4. Subtract (3 - 2) to get adjustment in doller-terms
  310. // 5. Use (2 + 4) as total cost, so (2) is "cost" and (4) is "adjustment"
  311. // Example:
  312. // - node.CPUCost = 10.00
  313. // - node.Discount = 0.20 // We assume a 20% discount
  314. // - adjustmentRate = 0.75 // CUR says we need to reduce to 75% of our post-discount node cost
  315. //
  316. // 1. See above
  317. // 2. discountedCPUCost = 10.00 * (1.0 - 0.2) = 8.00
  318. // 3. adjustedCPUCost = 8.00 * 0.75 = 6.00 // this is the actual cost according to the CUR
  319. // 4. adjustment = 6.00 - 8.00 = -2.00
  320. // 5. totalCost = 6.00, which is the sum of (2) cost = 8.00 and (4) adjustment = -2.00
  321. discountedCPUCost := node.CPUCost * (1.0 - node.Discount)
  322. adjustedCPUCost := discountedCPUCost * adjustmentRate
  323. cpuCostAdjustment := adjustedCPUCost - discountedCPUCost
  324. discountedRAMCost := node.RAMCost * (1.0 - node.Discount)
  325. adjustedRAMCost := discountedRAMCost * adjustmentRate
  326. ramCostAdjustment := adjustedRAMCost - discountedRAMCost
  327. adjustedGPUCost := node.GPUCost * adjustmentRate
  328. gpuCostAdjustment := adjustedGPUCost - node.GPUCost
  329. if _, ok := arts[key]; !ok {
  330. arts[key] = &AssetTotals{
  331. Start: node.Start,
  332. End: node.End,
  333. Cluster: node.Properties.Cluster,
  334. Node: node.Properties.Name,
  335. }
  336. }
  337. if arts[key].Start.After(node.Start) {
  338. arts[key].Start = node.Start
  339. }
  340. if arts[key].End.Before(node.End) {
  341. arts[key].End = node.End
  342. }
  343. if arts[key].Node != node.Properties.Name {
  344. arts[key].Node = ""
  345. }
  346. arts[key].Count++
  347. // TotalCPUCost will be discounted cost + adjustment
  348. arts[key].CPUCost += discountedCPUCost
  349. arts[key].CPUCostAdjustment += cpuCostAdjustment
  350. // TotalRAMCost will be discounted cost + adjustment
  351. arts[key].RAMCost += discountedRAMCost
  352. arts[key].RAMCostAdjustment += ramCostAdjustment
  353. // TotalGPUCost will be discounted cost + adjustment
  354. arts[key].GPUCost += node.GPUCost
  355. arts[key].GPUCostAdjustment += gpuCostAdjustment
  356. }
  357. for _, lb := range as.LoadBalancers {
  358. // Default to computing totals by Cluster, but allow override to use LoadBalancer.
  359. key := lb.Properties.Cluster
  360. if byAsset {
  361. key = fmt.Sprintf("%s/%s", lb.Properties.Cluster, lb.Properties.Name)
  362. }
  363. if _, ok := arts[key]; !ok {
  364. arts[key] = &AssetTotals{
  365. Start: lb.Start,
  366. End: lb.End,
  367. Cluster: lb.Properties.Cluster,
  368. Node: lb.Properties.Name,
  369. PrivateLoadBalancer: lb.Private,
  370. }
  371. }
  372. arts[key].LoadBalancerCost += lb.Cost
  373. arts[key].LoadBalancerCostAdjustment += lb.Adjustment
  374. }
  375. // Only record ClusterManagement when prop
  376. // is cluster. We can't breakdown these types by Node.
  377. if !byAsset {
  378. for _, cm := range as.ClusterManagement {
  379. key := cm.Properties.Cluster
  380. if _, ok := arts[key]; !ok {
  381. arts[key] = &AssetTotals{
  382. Start: cm.GetStart(),
  383. End: cm.GetEnd(),
  384. Cluster: cm.Properties.Cluster,
  385. }
  386. }
  387. arts[key].Count++
  388. arts[key].ClusterManagementCost += cm.Cost
  389. arts[key].ClusterManagementCostAdjustment += cm.Adjustment
  390. }
  391. }
  392. // Record disks in an intermediate structure, which will be
  393. // processed after all assets have been seen.
  394. for _, disk := range as.Disks {
  395. key := fmt.Sprintf("%s/%s", disk.Properties.Cluster, disk.Properties.Name)
  396. disks[key] = disk
  397. }
  398. // Record all disks as either attached volumes or persistent volumes.
  399. for name, disk := range disks {
  400. // By default, the key will be the name, which is the tuple of
  401. // cluster/node. But if we're aggregating by cluster only, then
  402. // reset the key to just the cluster.
  403. key := name
  404. if !byAsset {
  405. key = disk.Properties.Cluster
  406. }
  407. if _, ok := arts[key]; !ok {
  408. arts[key] = &AssetTotals{
  409. Start: disk.Start,
  410. End: disk.End,
  411. Cluster: disk.Properties.Cluster,
  412. }
  413. if byAsset {
  414. arts[key].Node = disk.Properties.Name
  415. }
  416. }
  417. _, isAttached := nodeNames[name]
  418. if isAttached {
  419. // Record attached volume data at the cluster and node level, using
  420. // name matching to distinguish from PersistentVolumes.
  421. // TODO can we make a stronger match at the underlying ETL layer?
  422. arts[key].Count++
  423. arts[key].AttachedVolumeCost += disk.Cost
  424. arts[key].AttachedVolumeCostAdjustment += disk.Adjustment
  425. } else {
  426. // Here, we're looking at a PersistentVolume because we're not
  427. // looking at an AttachedVolume.
  428. arts[key].Count++
  429. arts[key].PersistentVolumeCost += disk.Cost
  430. arts[key].PersistentVolumeCostAdjustment += disk.Adjustment
  431. }
  432. }
  433. return arts
  434. }
  435. // AssetTotalsSet represents totals, summed by both "cluster" and "node"
  436. // for a given window of time.
  437. type AssetTotalsSet struct {
  438. Cluster map[string]*AssetTotals `json:"cluster"`
  439. Node map[string]*AssetTotals `json:"node"`
  440. Window Window `json:"window"`
  441. }
  442. func NewAssetTotalsSet(window Window, byCluster, byNode map[string]*AssetTotals) *AssetTotalsSet {
  443. return &AssetTotalsSet{
  444. Cluster: byCluster,
  445. Node: byNode,
  446. Window: window.Clone(),
  447. }
  448. }
  449. // ComputeIdleCoefficients returns the idle coefficients for CPU, GPU, and RAM
  450. // (in that order) for the given resource costs and totals.
  451. func ComputeIdleCoefficients(shareSplit, key string, cpuCost, gpuCost, ramCost float64, allocationTotals map[string]*AllocationTotals) (float64, float64, float64) {
  452. if shareSplit == ShareNone {
  453. return 0.0, 0.0, 0.0
  454. }
  455. if shareSplit != ShareEven {
  456. shareSplit = ShareWeighted
  457. }
  458. var cpuCoeff, gpuCoeff, ramCoeff float64
  459. if _, ok := allocationTotals[key]; !ok {
  460. return 0.0, 0.0, 0.0
  461. }
  462. if shareSplit == ShareEven {
  463. coeff := 1.0 / float64(allocationTotals[key].Count)
  464. return coeff, coeff, coeff
  465. }
  466. if allocationTotals[key].TotalCPUCost() > 0 {
  467. cpuCoeff = cpuCost / allocationTotals[key].TotalCPUCost()
  468. }
  469. if allocationTotals[key].TotalGPUCost() > 0 {
  470. gpuCoeff = gpuCost / allocationTotals[key].TotalGPUCost()
  471. }
  472. if allocationTotals[key].TotalRAMCost() > 0 {
  473. ramCoeff = ramCost / allocationTotals[key].TotalRAMCost()
  474. }
  475. return cpuCoeff, gpuCoeff, ramCoeff
  476. }
  477. // TotalsStore acts as both an AllocationTotalsStore and an
  478. // AssetTotalsStore.
  479. type TotalsStore interface {
  480. AllocationTotalsStore
  481. AssetTotalsStore
  482. }
  483. // AllocationTotalsStore allows for storing (i.e. setting and
  484. // getting) AllocationTotals by cluster and by node.
  485. type AllocationTotalsStore interface {
  486. GetAllocationTotalsByCluster(start, end time.Time) (map[string]*AllocationTotals, bool)
  487. GetAllocationTotalsByNode(start, end time.Time) (map[string]*AllocationTotals, bool)
  488. SetAllocationTotalsByCluster(start, end time.Time, rts map[string]*AllocationTotals)
  489. SetAllocationTotalsByNode(start, end time.Time, rts map[string]*AllocationTotals)
  490. }
  491. // UpdateAllocationTotalsStore updates an AllocationTotalsStore
  492. // by totaling the given AllocationSet and saving the totals.
  493. func UpdateAllocationTotalsStore(arts AllocationTotalsStore, as *AllocationSet) (*AllocationTotalsSet, error) {
  494. if arts == nil {
  495. return nil, errors.New("cannot update nil AllocationTotalsStore")
  496. }
  497. if as == nil {
  498. return nil, errors.New("cannot update AllocationTotalsStore from nil AllocationSet")
  499. }
  500. if as.Window.IsOpen() {
  501. return nil, errors.New("cannot update AllocationTotalsStore from AllocationSet with open window")
  502. }
  503. start := *as.Window.Start()
  504. end := *as.Window.End()
  505. artsByCluster := ComputeAllocationTotals(as, AllocationClusterProp)
  506. arts.SetAllocationTotalsByCluster(start, end, artsByCluster)
  507. artsByNode := ComputeAllocationTotals(as, AllocationNodeProp)
  508. arts.SetAllocationTotalsByNode(start, end, artsByNode)
  509. log.Debugf("ETL: Allocation: updated resource totals for %s", as.Window)
  510. win := NewClosedWindow(start, end)
  511. abc := map[string]*AllocationTotals{}
  512. for key, val := range artsByCluster {
  513. abc[key] = val.Clone()
  514. }
  515. abn := map[string]*AllocationTotals{}
  516. for key, val := range artsByNode {
  517. abn[key] = val.Clone()
  518. }
  519. return NewAllocationTotalsSet(win, abc, abn), nil
  520. }
  521. // AssetTotalsStore allows for storing (i.e. setting and getting)
  522. // AssetTotals by cluster and by node.
  523. type AssetTotalsStore interface {
  524. GetAssetTotalsByCluster(start, end time.Time) (map[string]*AssetTotals, bool)
  525. GetAssetTotalsByNode(start, end time.Time) (map[string]*AssetTotals, bool)
  526. SetAssetTotalsByCluster(start, end time.Time, rts map[string]*AssetTotals)
  527. SetAssetTotalsByNode(start, end time.Time, rts map[string]*AssetTotals)
  528. }
  529. // UpdateAssetTotalsStore updates an AssetTotalsStore
  530. // by totaling the given AssetSet and saving the totals.
  531. func UpdateAssetTotalsStore(arts AssetTotalsStore, as *AssetSet) (*AssetTotalsSet, error) {
  532. if arts == nil {
  533. return nil, errors.New("cannot update nil AssetTotalsStore")
  534. }
  535. if as == nil {
  536. return nil, errors.New("cannot update AssetTotalsStore from nil AssetSet")
  537. }
  538. if as.Window.IsOpen() {
  539. return nil, errors.New("cannot update AssetTotalsStore from AssetSet with open window")
  540. }
  541. start := *as.Window.Start()
  542. end := *as.Window.End()
  543. artsByCluster := ComputeAssetTotals(as, false)
  544. arts.SetAssetTotalsByCluster(start, end, artsByCluster)
  545. artsByNode := ComputeAssetTotals(as, true)
  546. arts.SetAssetTotalsByNode(start, end, artsByNode)
  547. log.Debugf("ETL: Asset: updated resource totals for %s", as.Window)
  548. win := NewClosedWindow(start, end)
  549. abc := map[string]*AssetTotals{}
  550. for key, val := range artsByCluster {
  551. abc[key] = val.Clone()
  552. }
  553. abn := map[string]*AssetTotals{}
  554. for key, val := range artsByNode {
  555. abn[key] = val.Clone()
  556. }
  557. return NewAssetTotalsSet(win, abc, abn), nil
  558. }
  559. // MemoryTotalsStore is an in-memory cache TotalsStore
  560. type MemoryTotalsStore struct {
  561. allocTotalsByCluster *cache.Cache
  562. allocTotalsByNode *cache.Cache
  563. assetTotalsByCluster *cache.Cache
  564. assetTotalsByNode *cache.Cache
  565. }
  566. // NewMemoryTotalsStore instantiates a new MemoryTotalsStore,
  567. // which is composed of four in-memory caches.
  568. func NewMemoryTotalsStore() *MemoryTotalsStore {
  569. return &MemoryTotalsStore{
  570. allocTotalsByCluster: cache.New(cache.NoExpiration, cache.NoExpiration),
  571. allocTotalsByNode: cache.New(cache.NoExpiration, cache.NoExpiration),
  572. assetTotalsByCluster: cache.New(cache.NoExpiration, cache.NoExpiration),
  573. assetTotalsByNode: cache.New(cache.NoExpiration, cache.NoExpiration),
  574. }
  575. }
  576. // GetAllocationTotalsByCluster retrieves the AllocationTotals
  577. // by cluster for the given start and end times.
  578. func (mts *MemoryTotalsStore) GetAllocationTotalsByCluster(start time.Time, end time.Time) (map[string]*AllocationTotals, bool) {
  579. k := storeKey(start, end)
  580. if raw, ok := mts.allocTotalsByCluster.Get(k); !ok {
  581. return map[string]*AllocationTotals{}, false
  582. } else {
  583. original := raw.(map[string]*AllocationTotals)
  584. totals := make(map[string]*AllocationTotals, len(original))
  585. for k, v := range original {
  586. totals[k] = v.Clone()
  587. }
  588. return totals, true
  589. }
  590. }
  591. // GetAllocationTotalsByNode retrieves the AllocationTotals
  592. // by node for the given start and end times.
  593. func (mts *MemoryTotalsStore) GetAllocationTotalsByNode(start time.Time, end time.Time) (map[string]*AllocationTotals, bool) {
  594. k := storeKey(start, end)
  595. if raw, ok := mts.allocTotalsByNode.Get(k); !ok {
  596. return map[string]*AllocationTotals{}, false
  597. } else {
  598. original := raw.(map[string]*AllocationTotals)
  599. totals := make(map[string]*AllocationTotals, len(original))
  600. for k, v := range original {
  601. totals[k] = v.Clone()
  602. }
  603. return totals, true
  604. }
  605. }
  606. // SetAllocationTotalsByCluster set the per-cluster AllocationTotals
  607. // to the given values for the given start and end times.
  608. func (mts *MemoryTotalsStore) SetAllocationTotalsByCluster(start time.Time, end time.Time, arts map[string]*AllocationTotals) {
  609. k := storeKey(start, end)
  610. mts.allocTotalsByCluster.Set(k, arts, cache.NoExpiration)
  611. }
  612. // SetAllocationTotalsByNode set the per-node AllocationTotals
  613. // to the given values for the given start and end times.
  614. func (mts *MemoryTotalsStore) SetAllocationTotalsByNode(start time.Time, end time.Time, arts map[string]*AllocationTotals) {
  615. k := storeKey(start, end)
  616. mts.allocTotalsByNode.Set(k, arts, cache.NoExpiration)
  617. }
  618. // GetAssetTotalsByCluster retrieves the AssetTotals
  619. // by cluster for the given start and end times.
  620. func (mts *MemoryTotalsStore) GetAssetTotalsByCluster(start time.Time, end time.Time) (map[string]*AssetTotals, bool) {
  621. k := storeKey(start, end)
  622. if raw, ok := mts.assetTotalsByCluster.Get(k); !ok {
  623. return map[string]*AssetTotals{}, false
  624. } else {
  625. original := raw.(map[string]*AssetTotals)
  626. totals := make(map[string]*AssetTotals, len(original))
  627. for k, v := range original {
  628. totals[k] = v.Clone()
  629. }
  630. return totals, true
  631. }
  632. }
  633. // GetAssetTotalsByNode retrieves the AssetTotals
  634. // by node for the given start and end times.
  635. func (mts *MemoryTotalsStore) GetAssetTotalsByNode(start time.Time, end time.Time) (map[string]*AssetTotals, bool) {
  636. k := storeKey(start, end)
  637. if raw, ok := mts.assetTotalsByNode.Get(k); !ok {
  638. // it's possible that after accumulation, the time chunks stored here
  639. // are being queried combined
  640. return map[string]*AssetTotals{}, false
  641. } else {
  642. original := raw.(map[string]*AssetTotals)
  643. totals := make(map[string]*AssetTotals, len(original))
  644. for k, v := range original {
  645. totals[k] = v.Clone()
  646. }
  647. return totals, true
  648. }
  649. }
  650. // SetAssetTotalsByCluster set the per-cluster AssetTotals
  651. // to the given values for the given start and end times.
  652. func (mts *MemoryTotalsStore) SetAssetTotalsByCluster(start time.Time, end time.Time, arts map[string]*AssetTotals) {
  653. k := storeKey(start, end)
  654. mts.assetTotalsByCluster.Set(k, arts, cache.NoExpiration)
  655. }
  656. // SetAssetTotalsByNode set the per-node AssetTotals
  657. // to the given values for the given start and end times.
  658. func (mts *MemoryTotalsStore) SetAssetTotalsByNode(start time.Time, end time.Time, arts map[string]*AssetTotals) {
  659. k := storeKey(start, end)
  660. mts.assetTotalsByNode.Set(k, arts, cache.NoExpiration)
  661. }
  662. // storeKey creates a storage key based on start and end times
  663. func storeKey(start, end time.Time) string {
  664. startStr := strconv.FormatInt(start.Unix(), 10)
  665. endStr := strconv.FormatInt(end.Unix(), 10)
  666. return fmt.Sprintf("%s-%s", startStr, endStr)
  667. }