totals.go 28 KB

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