totals.go 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752
  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. ProviderID string `json:"providerID,omitempty"`
  182. Count int `json:"count"`
  183. AttachedVolumeCost float64 `json:"attachedVolumeCost"`
  184. AttachedVolumeCostAdjustment float64 `json:"attachedVolumeCostAdjustment"`
  185. ClusterManagementCost float64 `json:"clusterManagementCost"`
  186. ClusterManagementCostAdjustment float64 `json:"clusterManagementCostAdjustment"`
  187. CPUCost float64 `json:"cpuCost"`
  188. CPUCostAdjustment float64 `json:"cpuCostAdjustment"`
  189. GPUCost float64 `json:"gpuCost"`
  190. GPUCostAdjustment float64 `json:"gpuCostAdjustment"`
  191. LoadBalancerCost float64 `json:"loadBalancerCost"`
  192. LoadBalancerCostAdjustment float64 `json:"loadBalancerCostAdjustment"`
  193. PersistentVolumeCost float64 `json:"persistentVolumeCost"`
  194. PersistentVolumeCostAdjustment float64 `json:"persistentVolumeCostAdjustment"`
  195. RAMCost float64 `json:"ramCost"`
  196. RAMCostAdjustment float64 `json:"ramCostAdjustment"`
  197. PrivateLoadBalancer bool `json:"privateLoadBalancer"`
  198. }
  199. // ClearAdjustments sets all adjustment fields to 0.0
  200. func (art *AssetTotals) ClearAdjustments() {
  201. art.AttachedVolumeCostAdjustment = 0.0
  202. art.ClusterManagementCostAdjustment = 0.0
  203. art.CPUCostAdjustment = 0.0
  204. art.GPUCostAdjustment = 0.0
  205. art.LoadBalancerCostAdjustment = 0.0
  206. art.PersistentVolumeCostAdjustment = 0.0
  207. art.RAMCostAdjustment = 0.0
  208. }
  209. // Clone deep copies the AssetTotals
  210. func (art *AssetTotals) Clone() *AssetTotals {
  211. return &AssetTotals{
  212. Start: art.Start,
  213. End: art.End,
  214. Cluster: art.Cluster,
  215. Node: art.Node,
  216. ProviderID: art.ProviderID,
  217. Count: art.Count,
  218. AttachedVolumeCost: art.AttachedVolumeCost,
  219. AttachedVolumeCostAdjustment: art.AttachedVolumeCostAdjustment,
  220. ClusterManagementCost: art.ClusterManagementCost,
  221. ClusterManagementCostAdjustment: art.ClusterManagementCostAdjustment,
  222. CPUCost: art.CPUCost,
  223. CPUCostAdjustment: art.CPUCostAdjustment,
  224. GPUCost: art.GPUCost,
  225. GPUCostAdjustment: art.GPUCostAdjustment,
  226. LoadBalancerCost: art.LoadBalancerCost,
  227. LoadBalancerCostAdjustment: art.LoadBalancerCostAdjustment,
  228. PersistentVolumeCost: art.PersistentVolumeCost,
  229. PersistentVolumeCostAdjustment: art.PersistentVolumeCostAdjustment,
  230. RAMCost: art.RAMCost,
  231. RAMCostAdjustment: art.RAMCostAdjustment,
  232. PrivateLoadBalancer: art.PrivateLoadBalancer,
  233. }
  234. }
  235. // TotalAttachedVolumeCost returns CPU cost with adjustment.
  236. func (art *AssetTotals) TotalAttachedVolumeCost() float64 {
  237. return art.AttachedVolumeCost + art.AttachedVolumeCostAdjustment
  238. }
  239. // TotalClusterManagementCost returns ClusterManagement cost with adjustment.
  240. func (art *AssetTotals) TotalClusterManagementCost() float64 {
  241. return art.ClusterManagementCost + art.ClusterManagementCostAdjustment
  242. }
  243. // TotalCPUCost returns CPU cost with adjustment.
  244. func (art *AssetTotals) TotalCPUCost() float64 {
  245. return art.CPUCost + art.CPUCostAdjustment
  246. }
  247. // TotalGPUCost returns GPU cost with adjustment.
  248. func (art *AssetTotals) TotalGPUCost() float64 {
  249. return art.GPUCost + art.GPUCostAdjustment
  250. }
  251. // TotalLoadBalancerCost returns LoadBalancer cost with adjustment.
  252. func (art *AssetTotals) TotalLoadBalancerCost() float64 {
  253. return art.LoadBalancerCost + art.LoadBalancerCostAdjustment
  254. }
  255. // TotalPersistentVolumeCost returns PersistentVolume cost with adjustment.
  256. func (art *AssetTotals) TotalPersistentVolumeCost() float64 {
  257. return art.PersistentVolumeCost + art.PersistentVolumeCostAdjustment
  258. }
  259. // TotalRAMCost returns RAM cost with adjustment.
  260. func (art *AssetTotals) TotalRAMCost() float64 {
  261. return art.RAMCost + art.RAMCostAdjustment
  262. }
  263. // TotalCost returns the sum of all costs
  264. func (art *AssetTotals) TotalCost() float64 {
  265. return art.TotalAttachedVolumeCost() + art.TotalClusterManagementCost() +
  266. art.TotalCPUCost() + art.TotalGPUCost() + art.TotalLoadBalancerCost() +
  267. art.TotalPersistentVolumeCost() + art.TotalRAMCost()
  268. }
  269. // ComputeAssetTotals totals the resource costs of the given AssetSet,
  270. // using the given property, i.e. cluster or node, where "node" really means to
  271. // use the fully-qualified (cluster, node) tuple.
  272. // NOTE: we're not capturing LoadBalancers here yet, but only because we don't
  273. // yet need them. They could be added.
  274. func ComputeAssetTotals(as *AssetSet, byAsset bool) map[string]*AssetTotals {
  275. arts := map[string]*AssetTotals{}
  276. // Attached disks are tracked by matching their name with the name of the
  277. // node, as is standard for attached disks.
  278. nodeNames := map[string]bool{}
  279. disks := map[string]*Disk{}
  280. for _, node := range as.Nodes {
  281. // Default to computing totals by Cluster, but allow override to use Node.
  282. key := node.Properties.Cluster
  283. if byAsset {
  284. key = fmt.Sprintf("%s/%s", node.Properties.Cluster, node.Properties.Name)
  285. }
  286. // Add node name to list of node names. (These are to be used later
  287. // for attached volumes.)
  288. nodeNames[fmt.Sprintf("%s/%s", node.Properties.Cluster, node.Properties.Name)] = true
  289. // adjustmentRate is used to scale resource costs proportionally
  290. // by the adjustment. This is necessary because we only get one
  291. // adjustment per Node, not one per-resource-per-Node.
  292. //
  293. // e.g. total cost = $90 (cost = $100, adjustment = -$10) => 0.9000 ( 90 / 100)
  294. // e.g. total cost = $150 (cost = $450, adjustment = -$300) => 0.3333 (150 / 450)
  295. // e.g. total cost = $150 (cost = $100, adjustment = $50) => 1.5000 (150 / 100)
  296. adjustmentRate := 1.0
  297. if node.TotalCost()-node.Adjustment == 0 {
  298. // If (totalCost - adjustment) is 0.0 then adjustment cancels
  299. // the entire node cost and we should make everything 0
  300. // without dividing by 0.
  301. adjustmentRate = 0.0
  302. log.DedupedWarningf(5, "ComputeTotals: node cost adjusted to $0.00 for %s", node.Properties.Name)
  303. } else if node.Adjustment != 0.0 {
  304. // adjustmentRate is the ratio of cost-with-adjustment (i.e. TotalCost)
  305. // to cost-without-adjustment (i.e. TotalCost - Adjustment).
  306. adjustmentRate = node.TotalCost() / (node.TotalCost() - node.Adjustment)
  307. }
  308. // 1. Start with raw, measured resource cost
  309. // 2. Apply discount to get discounted resource cost
  310. // 3. Apply adjustment to get final "adjusted" resource cost
  311. // 4. Subtract (3 - 2) to get adjustment in doller-terms
  312. // 5. Use (2 + 4) as total cost, so (2) is "cost" and (4) is "adjustment"
  313. // Example:
  314. // - node.CPUCost = 10.00
  315. // - node.Discount = 0.20 // We assume a 20% discount
  316. // - adjustmentRate = 0.75 // CUR says we need to reduce to 75% of our post-discount node cost
  317. //
  318. // 1. See above
  319. // 2. discountedCPUCost = 10.00 * (1.0 - 0.2) = 8.00
  320. // 3. adjustedCPUCost = 8.00 * 0.75 = 6.00 // this is the actual cost according to the CUR
  321. // 4. adjustment = 6.00 - 8.00 = -2.00
  322. // 5. totalCost = 6.00, which is the sum of (2) cost = 8.00 and (4) adjustment = -2.00
  323. discountedCPUCost := node.CPUCost * (1.0 - node.Discount)
  324. adjustedCPUCost := discountedCPUCost * adjustmentRate
  325. cpuCostAdjustment := adjustedCPUCost - discountedCPUCost
  326. discountedRAMCost := node.RAMCost * (1.0 - node.Discount)
  327. adjustedRAMCost := discountedRAMCost * adjustmentRate
  328. ramCostAdjustment := adjustedRAMCost - discountedRAMCost
  329. adjustedGPUCost := node.GPUCost * adjustmentRate
  330. gpuCostAdjustment := adjustedGPUCost - node.GPUCost
  331. var providerID string
  332. if byAsset && node.Properties.ProviderID != "" {
  333. providerID = node.Properties.ProviderID
  334. }
  335. if _, ok := arts[key]; !ok {
  336. arts[key] = &AssetTotals{
  337. Start: node.Start,
  338. End: node.End,
  339. Cluster: node.Properties.Cluster,
  340. Node: node.Properties.Name,
  341. ProviderID: providerID,
  342. }
  343. }
  344. if arts[key].Start.After(node.Start) {
  345. arts[key].Start = node.Start
  346. }
  347. if arts[key].End.Before(node.End) {
  348. arts[key].End = node.End
  349. }
  350. if arts[key].Node != node.Properties.Name {
  351. arts[key].Node = ""
  352. }
  353. arts[key].Count++
  354. // TotalCPUCost will be discounted cost + adjustment
  355. arts[key].CPUCost += discountedCPUCost
  356. arts[key].CPUCostAdjustment += cpuCostAdjustment
  357. // TotalRAMCost will be discounted cost + adjustment
  358. arts[key].RAMCost += discountedRAMCost
  359. arts[key].RAMCostAdjustment += ramCostAdjustment
  360. // TotalGPUCost will be discounted cost + adjustment
  361. arts[key].GPUCost += node.GPUCost
  362. arts[key].GPUCostAdjustment += gpuCostAdjustment
  363. }
  364. for _, lb := range as.LoadBalancers {
  365. // Default to computing totals by Cluster, but allow override to use LoadBalancer.
  366. key := lb.Properties.Cluster
  367. if byAsset {
  368. key = fmt.Sprintf("%s/%s", lb.Properties.Cluster, lb.Properties.Name)
  369. }
  370. if _, ok := arts[key]; !ok {
  371. arts[key] = &AssetTotals{
  372. Start: lb.Start,
  373. End: lb.End,
  374. Cluster: lb.Properties.Cluster,
  375. Node: lb.Properties.Name,
  376. PrivateLoadBalancer: lb.Private,
  377. }
  378. }
  379. arts[key].LoadBalancerCost += lb.Cost
  380. arts[key].LoadBalancerCostAdjustment += lb.Adjustment
  381. }
  382. // Only record ClusterManagement when prop
  383. // is cluster. We can't breakdown these types by Node.
  384. if !byAsset {
  385. for _, cm := range as.ClusterManagement {
  386. key := cm.Properties.Cluster
  387. if _, ok := arts[key]; !ok {
  388. arts[key] = &AssetTotals{
  389. Start: cm.GetStart(),
  390. End: cm.GetEnd(),
  391. Cluster: cm.Properties.Cluster,
  392. }
  393. }
  394. arts[key].Count++
  395. arts[key].ClusterManagementCost += cm.Cost
  396. arts[key].ClusterManagementCostAdjustment += cm.Adjustment
  397. }
  398. }
  399. // Record disks in an intermediate structure, which will be
  400. // processed after all assets have been seen.
  401. for _, disk := range as.Disks {
  402. key := fmt.Sprintf("%s/%s", disk.Properties.Cluster, disk.Properties.Name)
  403. disks[key] = disk
  404. }
  405. // Record all disks as either attached volumes or persistent volumes.
  406. for name, disk := range disks {
  407. // By default, the key will be the name, which is the tuple of
  408. // cluster/node. But if we're aggregating by cluster only, then
  409. // reset the key to just the cluster.
  410. key := name
  411. if !byAsset {
  412. key = disk.Properties.Cluster
  413. }
  414. if _, ok := arts[key]; !ok {
  415. arts[key] = &AssetTotals{
  416. Start: disk.Start,
  417. End: disk.End,
  418. Cluster: disk.Properties.Cluster,
  419. }
  420. if byAsset {
  421. arts[key].Node = disk.Properties.Name
  422. }
  423. }
  424. _, isAttached := nodeNames[name]
  425. if isAttached {
  426. // Record attached volume data at the cluster and node level, using
  427. // name matching to distinguish from PersistentVolumes.
  428. // TODO can we make a stronger match at the underlying costmodel layer?
  429. arts[key].Count++
  430. arts[key].AttachedVolumeCost += disk.Cost
  431. arts[key].AttachedVolumeCostAdjustment += disk.Adjustment
  432. } else {
  433. // Here, we're looking at a PersistentVolume because we're not
  434. // looking at an AttachedVolume.
  435. arts[key].Count++
  436. arts[key].PersistentVolumeCost += disk.Cost
  437. arts[key].PersistentVolumeCostAdjustment += disk.Adjustment
  438. }
  439. }
  440. return arts
  441. }
  442. // AssetTotalsSet represents totals, summed by both "cluster" and "node"
  443. // for a given window of time.
  444. type AssetTotalsSet struct {
  445. Cluster map[string]*AssetTotals `json:"cluster"`
  446. Node map[string]*AssetTotals `json:"node"`
  447. Window Window `json:"window"`
  448. }
  449. func NewAssetTotalsSet(window Window, byCluster, byNode map[string]*AssetTotals) *AssetTotalsSet {
  450. return &AssetTotalsSet{
  451. Cluster: byCluster,
  452. Node: byNode,
  453. Window: window.Clone(),
  454. }
  455. }
  456. // ComputeIdleCoefficients returns the idle coefficients for CPU, GPU, and RAM
  457. // (in that order) for the given resource costs and totals.
  458. func ComputeIdleCoefficients(shareSplit, key string, cpuCost, gpuCost, ramCost float64, allocationTotals map[string]*AllocationTotals) (float64, float64, float64) {
  459. if shareSplit == ShareNone {
  460. return 0.0, 0.0, 0.0
  461. }
  462. if shareSplit != ShareEven {
  463. shareSplit = ShareWeighted
  464. }
  465. var cpuCoeff, gpuCoeff, ramCoeff float64
  466. if _, ok := allocationTotals[key]; !ok {
  467. return 0.0, 0.0, 0.0
  468. }
  469. if shareSplit == ShareEven {
  470. coeff := 1.0 / float64(allocationTotals[key].Count)
  471. return coeff, coeff, coeff
  472. }
  473. if allocationTotals[key].TotalCPUCost() > 0 {
  474. cpuCoeff = cpuCost / allocationTotals[key].TotalCPUCost()
  475. }
  476. if allocationTotals[key].TotalGPUCost() > 0 {
  477. gpuCoeff = gpuCost / allocationTotals[key].TotalGPUCost()
  478. }
  479. if allocationTotals[key].TotalRAMCost() > 0 {
  480. ramCoeff = ramCost / allocationTotals[key].TotalRAMCost()
  481. }
  482. return cpuCoeff, gpuCoeff, ramCoeff
  483. }
  484. // TotalsStore acts as both an AllocationTotalsStore and an
  485. // AssetTotalsStore.
  486. type TotalsStore interface {
  487. AllocationTotalsStore
  488. AssetTotalsStore
  489. }
  490. // AllocationTotalsStore allows for storing (i.e. setting and
  491. // getting) AllocationTotals by cluster and by node.
  492. type AllocationTotalsStore interface {
  493. GetAllocationTotalsByCluster(start, end time.Time) (map[string]*AllocationTotals, bool)
  494. GetAllocationTotalsByNode(start, end time.Time) (map[string]*AllocationTotals, bool)
  495. SetAllocationTotalsByCluster(start, end time.Time, rts map[string]*AllocationTotals)
  496. SetAllocationTotalsByNode(start, end time.Time, rts map[string]*AllocationTotals)
  497. }
  498. // AssetTotalsStore allows for storing (i.e. setting and getting)
  499. // AssetTotals by cluster and by node.
  500. type AssetTotalsStore interface {
  501. GetAssetTotalsByCluster(start, end time.Time) (map[string]*AssetTotals, bool)
  502. GetAssetTotalsByNode(start, end time.Time) (map[string]*AssetTotals, bool)
  503. SetAssetTotalsByCluster(start, end time.Time, rts map[string]*AssetTotals)
  504. SetAssetTotalsByNode(start, end time.Time, rts map[string]*AssetTotals)
  505. }
  506. // UpdateAssetTotalsStore updates an AssetTotalsStore
  507. // by totaling the given AssetSet and saving the totals.
  508. func UpdateAssetTotalsStore(arts AssetTotalsStore, as *AssetSet) (*AssetTotalsSet, error) {
  509. if arts == nil {
  510. return nil, errors.New("cannot update nil AssetTotalsStore")
  511. }
  512. if as == nil {
  513. return nil, errors.New("cannot update AssetTotalsStore from nil AssetSet")
  514. }
  515. if as.Window.IsOpen() {
  516. return nil, errors.New("cannot update AssetTotalsStore from AssetSet with open window")
  517. }
  518. start := *as.Window.Start()
  519. end := *as.Window.End()
  520. artsByCluster := ComputeAssetTotals(as, false)
  521. arts.SetAssetTotalsByCluster(start, end, artsByCluster)
  522. artsByNode := ComputeAssetTotals(as, true)
  523. arts.SetAssetTotalsByNode(start, end, artsByNode)
  524. log.Debugf("Asset: updated resource totals for %s", as.Window)
  525. win := NewClosedWindow(start, end)
  526. abc := map[string]*AssetTotals{}
  527. for key, val := range artsByCluster {
  528. abc[key] = val.Clone()
  529. }
  530. abn := map[string]*AssetTotals{}
  531. for key, val := range artsByNode {
  532. abn[key] = val.Clone()
  533. }
  534. return NewAssetTotalsSet(win, abc, abn), nil
  535. }
  536. // MemoryTotalsStore is an in-memory cache TotalsStore
  537. type MemoryTotalsStore struct {
  538. allocTotalsByCluster *cache.Cache
  539. allocTotalsByNode *cache.Cache
  540. assetTotalsByCluster *cache.Cache
  541. assetTotalsByNode *cache.Cache
  542. }
  543. // NewMemoryTotalsStore instantiates a new MemoryTotalsStore,
  544. // which is composed of four in-memory caches.
  545. func NewMemoryTotalsStore() *MemoryTotalsStore {
  546. return &MemoryTotalsStore{
  547. allocTotalsByCluster: cache.New(cache.NoExpiration, cache.NoExpiration),
  548. allocTotalsByNode: cache.New(cache.NoExpiration, cache.NoExpiration),
  549. assetTotalsByCluster: cache.New(cache.NoExpiration, cache.NoExpiration),
  550. assetTotalsByNode: cache.New(cache.NoExpiration, cache.NoExpiration),
  551. }
  552. }
  553. // GetAllocationTotalsByCluster retrieves the AllocationTotals
  554. // by cluster for the given start and end times.
  555. func (mts *MemoryTotalsStore) GetAllocationTotalsByCluster(start time.Time, end time.Time) (map[string]*AllocationTotals, bool) {
  556. k := storeKey(start, end)
  557. if raw, ok := mts.allocTotalsByCluster.Get(k); !ok {
  558. return map[string]*AllocationTotals{}, false
  559. } else {
  560. original := raw.(map[string]*AllocationTotals)
  561. totals := make(map[string]*AllocationTotals, len(original))
  562. for k, v := range original {
  563. totals[k] = v.Clone()
  564. }
  565. return totals, true
  566. }
  567. }
  568. // GetAllocationTotalsByNode retrieves the AllocationTotals
  569. // by node for the given start and end times.
  570. func (mts *MemoryTotalsStore) GetAllocationTotalsByNode(start time.Time, end time.Time) (map[string]*AllocationTotals, bool) {
  571. k := storeKey(start, end)
  572. if raw, ok := mts.allocTotalsByNode.Get(k); !ok {
  573. return map[string]*AllocationTotals{}, false
  574. } else {
  575. original := raw.(map[string]*AllocationTotals)
  576. totals := make(map[string]*AllocationTotals, len(original))
  577. for k, v := range original {
  578. totals[k] = v.Clone()
  579. }
  580. return totals, true
  581. }
  582. }
  583. // SetAllocationTotalsByCluster set the per-cluster AllocationTotals
  584. // to the given values for the given start and end times.
  585. func (mts *MemoryTotalsStore) SetAllocationTotalsByCluster(start time.Time, end time.Time, arts map[string]*AllocationTotals) {
  586. k := storeKey(start, end)
  587. mts.allocTotalsByCluster.Set(k, arts, cache.NoExpiration)
  588. }
  589. // SetAllocationTotalsByNode set the per-node AllocationTotals
  590. // to the given values for the given start and end times.
  591. func (mts *MemoryTotalsStore) SetAllocationTotalsByNode(start time.Time, end time.Time, arts map[string]*AllocationTotals) {
  592. k := storeKey(start, end)
  593. mts.allocTotalsByNode.Set(k, arts, cache.NoExpiration)
  594. }
  595. // GetAssetTotalsByCluster retrieves the AssetTotals
  596. // by cluster for the given start and end times.
  597. func (mts *MemoryTotalsStore) GetAssetTotalsByCluster(start time.Time, end time.Time) (map[string]*AssetTotals, bool) {
  598. k := storeKey(start, end)
  599. if raw, ok := mts.assetTotalsByCluster.Get(k); !ok {
  600. return map[string]*AssetTotals{}, false
  601. } else {
  602. original := raw.(map[string]*AssetTotals)
  603. totals := make(map[string]*AssetTotals, len(original))
  604. for k, v := range original {
  605. totals[k] = v.Clone()
  606. }
  607. return totals, true
  608. }
  609. }
  610. // GetAssetTotalsByNode retrieves the AssetTotals
  611. // by node for the given start and end times.
  612. func (mts *MemoryTotalsStore) GetAssetTotalsByNode(start time.Time, end time.Time) (map[string]*AssetTotals, bool) {
  613. k := storeKey(start, end)
  614. if raw, ok := mts.assetTotalsByNode.Get(k); !ok {
  615. // it's possible that after accumulation, the time chunks stored here
  616. // are being queried combined
  617. return map[string]*AssetTotals{}, false
  618. } else {
  619. original := raw.(map[string]*AssetTotals)
  620. totals := make(map[string]*AssetTotals, len(original))
  621. for k, v := range original {
  622. totals[k] = v.Clone()
  623. }
  624. return totals, true
  625. }
  626. }
  627. // SetAssetTotalsByCluster set the per-cluster AssetTotals
  628. // to the given values for the given start and end times.
  629. func (mts *MemoryTotalsStore) SetAssetTotalsByCluster(start time.Time, end time.Time, arts map[string]*AssetTotals) {
  630. k := storeKey(start, end)
  631. mts.assetTotalsByCluster.Set(k, arts, cache.NoExpiration)
  632. }
  633. // SetAssetTotalsByNode set the per-node AssetTotals
  634. // to the given values for the given start and end times.
  635. func (mts *MemoryTotalsStore) SetAssetTotalsByNode(start time.Time, end time.Time, arts map[string]*AssetTotals) {
  636. k := storeKey(start, end)
  637. mts.assetTotalsByNode.Set(k, arts, cache.NoExpiration)
  638. }
  639. // storeKey creates a storage key based on start and end times
  640. func storeKey(start, end time.Time) string {
  641. startStr := strconv.FormatInt(start.Unix(), 10)
  642. endStr := strconv.FormatInt(end.Unix(), 10)
  643. return fmt.Sprintf("%s-%s", startStr, endStr)
  644. }