-
Notifications
You must be signed in to change notification settings - Fork 85
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
295 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
84 changes: 84 additions & 0 deletions
84
src/main/java/io/codeka/gaia/service/StackCostCalculator.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package io.codeka.gaia.service; | ||
|
||
import io.codeka.gaia.bo.JobStatus; | ||
import io.codeka.gaia.bo.JobType; | ||
import io.codeka.gaia.bo.Stack; | ||
import io.codeka.gaia.repository.JobRepository; | ||
import io.codeka.gaia.repository.TerraformModuleRepository; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.stereotype.Service; | ||
|
||
import java.math.BigDecimal; | ||
import java.math.RoundingMode; | ||
import java.time.Duration; | ||
import java.time.LocalDateTime; | ||
|
||
/** | ||
* This service calculates the cost of the running stack | ||
*/ | ||
@Service | ||
public class StackCostCalculator { | ||
|
||
private JobRepository jobRepository; | ||
|
||
private TerraformModuleRepository moduleRepository; | ||
|
||
@Autowired | ||
public StackCostCalculator(JobRepository jobRepository, TerraformModuleRepository moduleRepository) { | ||
this.jobRepository = jobRepository; | ||
this.moduleRepository = moduleRepository; | ||
} | ||
|
||
/** | ||
* Calculates an estimation of what this stack has cost in total | ||
* @param stack | ||
* @return | ||
*/ | ||
public BigDecimal calculateRunningCostEstimation(Stack stack){ | ||
var jobs = jobRepository.findAllByStackId(stack.getId()); | ||
|
||
if(jobs.isEmpty()){ | ||
return BigDecimal.ZERO; | ||
} | ||
|
||
var module = moduleRepository.findById(stack.getModuleId()).orElseThrow(); | ||
|
||
if(module.getEstimatedMonthlyCost() == null){ | ||
return BigDecimal.ZERO; | ||
} | ||
|
||
// calculate total running time | ||
var duration = Duration.ofDays(0); | ||
|
||
var start = LocalDateTime.now(); | ||
var end = LocalDateTime.now(); | ||
|
||
for(var job : jobs ){ | ||
// add | ||
if(job.getType() == JobType.RUN && job.getStatus() == JobStatus.FINISHED){ | ||
start =job.getStartDateTime(); | ||
end = LocalDateTime.now(); | ||
} | ||
if(job.getType() == JobType.STOP && job.getStatus() == JobStatus.FINISHED){ | ||
end = job.getStartDateTime(); | ||
duration = duration.plus(Duration.between(start, end)); | ||
|
||
// reset start and end | ||
start = LocalDateTime.now(); | ||
end = LocalDateTime.now(); | ||
} | ||
} | ||
|
||
// add last duration | ||
duration = duration.plus(Duration.between(start, end)); | ||
|
||
// get hourly cost | ||
var hourlyCost = module.getEstimatedMonthlyCost().divide(BigDecimal.valueOf(31L*24L), 4, RoundingMode.HALF_UP); | ||
|
||
// get total cost | ||
return hourlyCost.multiply(BigDecimal.valueOf(duration.toHours())) | ||
// round it to 2 decimals | ||
.setScale(2, RoundingMode.HALF_UP); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
src/test/java/io/codeka/gaia/service/StackCostCalculatorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package io.codeka.gaia.service; | ||
|
||
import io.codeka.gaia.bo.Job; | ||
import io.codeka.gaia.bo.JobType; | ||
import io.codeka.gaia.bo.Stack; | ||
import io.codeka.gaia.bo.TerraformModule; | ||
import io.codeka.gaia.repository.JobRepository; | ||
import io.codeka.gaia.repository.TerraformModuleRepository; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.InjectMocks; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
import java.math.BigDecimal; | ||
import java.time.LocalDateTime; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class StackCostCalculatorTest { | ||
|
||
@Mock | ||
private JobRepository jobRepository; | ||
|
||
@Mock | ||
private TerraformModuleRepository moduleRepository; | ||
|
||
@InjectMocks | ||
private StackCostCalculator calculator; | ||
|
||
@Test | ||
void stacksWithNoJob_shouldHaveZeroCost(){ | ||
// given | ||
var stack = new Stack(); | ||
stack.setId("12"); | ||
|
||
when(jobRepository.findAllByStackId("12")).thenReturn(Collections.EMPTY_LIST); | ||
|
||
// when | ||
var cost = calculator.calculateRunningCostEstimation(stack); | ||
|
||
// then | ||
assertEquals(BigDecimal.ZERO, cost); | ||
} | ||
|
||
@Test | ||
void stacksWithOneRunJob_shouldHaveCostEqualToRunningTime(){ | ||
// given | ||
var stack = new Stack(); | ||
stack.setId("12"); | ||
stack.setModuleId("42"); | ||
|
||
var module = new TerraformModule(); | ||
module.setEstimatedMonthlyCost(BigDecimal.valueOf(31)); | ||
when(moduleRepository.findById("42")).thenReturn(Optional.of(module)); | ||
|
||
// a job started two days ago | ||
var job = new Job(); | ||
job.start(JobType.RUN); | ||
job.end(); | ||
job.setStartDateTime(LocalDateTime.now().minusDays(2)); | ||
when(jobRepository.findAllByStackId("12")).thenReturn(List.of(job)); | ||
|
||
// when | ||
var cost = calculator.calculateRunningCostEstimation(stack); | ||
|
||
// then | ||
assertEquals(BigDecimal.valueOf(2).setScale(2), cost); | ||
} | ||
|
||
@Test | ||
void stacksWithOneRunJobAndOneStopJob_shouldHaveCostEqualToRunningTime(){ | ||
// given | ||
var stack = new Stack(); | ||
stack.setId("12"); | ||
stack.setModuleId("42"); | ||
|
||
var module = new TerraformModule(); | ||
module.setEstimatedMonthlyCost(BigDecimal.valueOf(31)); | ||
when(moduleRepository.findById("42")).thenReturn(Optional.of(module)); | ||
|
||
// a job started two days ago | ||
var job = new Job(); | ||
job.start(JobType.RUN); | ||
job.end(); | ||
job.setStartDateTime(LocalDateTime.now().minusDays(2)); | ||
|
||
// a job stopped one day ago | ||
var jobStop = new Job(); | ||
jobStop.start(JobType.STOP); | ||
jobStop.end(); | ||
jobStop.setStartDateTime(LocalDateTime.now().minusDays(1)); | ||
|
||
when(jobRepository.findAllByStackId("12")).thenReturn(List.of(job, jobStop)); | ||
|
||
// when | ||
var cost = calculator.calculateRunningCostEstimation(stack); | ||
|
||
// then | ||
assertEquals(BigDecimal.valueOf(1).setScale(2), cost); | ||
} | ||
|
||
@Test | ||
void stacksWithOneRunJobAndOneStopJobAndRelaunchedOneHourAgo_shouldHaveCostEqualToRunningTime(){ | ||
// given | ||
var stack = new Stack(); | ||
stack.setId("12"); | ||
stack.setModuleId("42"); | ||
|
||
var module = new TerraformModule(); | ||
module.setEstimatedMonthlyCost(BigDecimal.valueOf(31)); | ||
when(moduleRepository.findById("42")).thenReturn(Optional.of(module)); | ||
|
||
// a job started two days ago | ||
var job = new Job(); | ||
job.start(JobType.RUN); | ||
job.end(); | ||
job.setStartDateTime(LocalDateTime.now().minusDays(2)); | ||
|
||
// a job stopped one day ago | ||
var jobStop = new Job(); | ||
jobStop.start(JobType.STOP); | ||
jobStop.end(); | ||
jobStop.setStartDateTime(LocalDateTime.now().minusDays(1)); | ||
|
||
// a job started 6 hours ago | ||
var jobRelaunch = new Job(); | ||
jobRelaunch.start(JobType.RUN); | ||
jobRelaunch.end(); | ||
jobRelaunch.setStartDateTime(LocalDateTime.now().minusHours(1)); | ||
|
||
when(jobRepository.findAllByStackId("12")).thenReturn(List.of(job, jobStop, jobRelaunch)); | ||
|
||
// when | ||
var cost = calculator.calculateRunningCostEstimation(stack); | ||
|
||
// then | ||
assertEquals(BigDecimal.valueOf(1.04).setScale(2), cost); | ||
} | ||
|
||
@Test | ||
void stacksWithModuleHavingNoCost_shouldHaveZeroCost(){ | ||
// given | ||
var stack = new Stack(); | ||
stack.setId("12"); | ||
stack.setModuleId("42"); | ||
|
||
// a job started two days ago | ||
var job = new Job(); | ||
job.start(JobType.RUN); | ||
job.end(); | ||
job.setStartDateTime(LocalDateTime.now().minusDays(2)); | ||
when(jobRepository.findAllByStackId("12")).thenReturn(List.of(job)); | ||
|
||
// but a module with no cost | ||
var module = new TerraformModule(); | ||
when(moduleRepository.findById("42")).thenReturn(Optional.of(module)); | ||
|
||
// when | ||
var cost = calculator.calculateRunningCostEstimation(stack); | ||
|
||
// then | ||
assertEquals(BigDecimal.ZERO, cost); | ||
} | ||
|
||
} |