diff --git a/litmus-core/src/main/java/com/navi/medici/Constants.java b/litmus-core/src/main/java/com/navi/medici/Constants.java index 0794b51..261eef1 100644 --- a/litmus-core/src/main/java/com/navi/medici/Constants.java +++ b/litmus-core/src/main/java/com/navi/medici/Constants.java @@ -5,6 +5,7 @@ public class Constants { public static final String HEADER_EMAIL_ID = "X-Email-Id"; public static final String CONTROL = "CONTROL"; public static final String TREATMENT = "TREATMENT"; + public static final String POPULATION_METRIC = "population-metric"; //Update Messages public static final String EXPERIMENT_CREATED = "Experiment created"; @@ -15,8 +16,9 @@ public class Constants { public static final String PAUSE_EXPERIMENT = "Experiment has been paused"; public static final String METRIC_ATTACHED = "Metric %s attached to experiment"; public static final String EXPERIMENT_RESTARTED = "Experiment Restarted"; + public static final String METRICS_RESET = "All the metrics have been reset"; - //Necessary Query Variables + //Query Variables public static final String EXPERIMENT_NAME = "experimentName"; public static final String EXPERIMENT_CREATED_AT = "experimentCreatedAt"; public static final String EXPERIMENT_ID = "experimentId"; @@ -25,5 +27,8 @@ public class Constants { public static final String START_TIME = "startTime"; public static final String END_TIME = "endTime"; public static final String DAY_INTERVAL_FOR_TOTAL_USERS = "dayIntervalForTotalUsers"; - + public static final String VARIANT_NAMES = "variantNames"; + public static final String ARE_VARIANTS_PRESENT = "areVariantsPresent"; + public static final String QUERY_TYPE = "queryType"; + } diff --git a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java index 3e42f0e..dfa7fcc 100644 --- a/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java +++ b/litmus-core/src/main/java/com/navi/medici/config/LitmusCoreConfig.java @@ -61,6 +61,12 @@ public class LitmusCoreConfig { @Value("${tesseract.interval.function.initial.interval}") long intervalFunctionInitialInterval; + @Value("${experiment.metric.fetch.limit.per.interval}") + private int experimentMetricFetchLimitPerInterval; + + @Value("${experiment.population.fetch.limit.per.interval}") + private int experimentPopulationFetchLimitPerInterval; + @Value("${tesseract.interval.function.max.interval}") long intervalFunctionMaxInterval; diff --git a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java index c7cc84d..6fbbb00 100644 --- a/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java +++ b/litmus-core/src/main/java/com/navi/medici/controller/v2/ExperimentControllerV2.java @@ -13,6 +13,7 @@ import com.navi.medici.response.ExperimentResponse; import com.navi.medici.response.PaginatedSearchResponse; import com.navi.medici.service.experiment.ExperimentService; import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import com.navi.medici.service.experimentpopulationresult.ExperimentPopulationResultService; import com.navi.medici.tesseract.response.TesseractIdStatusResponse; import com.navi.medici.util.ControllerUtils; import io.micrometer.core.annotation.Timed; @@ -20,6 +21,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -41,6 +43,8 @@ public class ExperimentControllerV2 { private final ExperimentService experimentService; private final ExperimentMetricResultService experimentMetricResultService; + private final ExperimentPopulationResultService experimentPopulationResultService; + @GetMapping @Timed(value = "litmus.get.experiments", percentiles = {0.95, 0.99}) public ResponseEntity> getExperiments(@RequestParam(value = "query", required = false) String query, @@ -146,10 +150,17 @@ public class ExperimentControllerV2 { @PutMapping("/refresh-metric") @Timed(value = "experiment.refresh.metric.result", percentiles = {0.95, 0.99}) public void refreshMetricResultOfExperiment(@RequestParam("name") String experimentName) { - log.info("refresh metric request result received for experiment: {}", experimentName); + log.info("refresh metric result received for experiment: {}", experimentName); experimentMetricResultService.refreshMetricResult(experimentName); } + @PutMapping("/refresh-population") + @Timed(value = "experiment.refresh.population.result", percentiles = {0.95, 0.99}) + public void refreshPopulationResultOfExperiment(@RequestParam("name") String experimentName) { + log.info("refresh population result received for experiment: {}", experimentName); + experimentPopulationResultService.refreshPopulationResult(experimentName); + } + @PutMapping("/toggle/{experimentId}") @Timed(value = "experiment.enabled", percentiles = {0.95, 0.99}) public void toggleExperiment(@PathVariable("experimentId") String experimentId, @@ -159,4 +170,12 @@ public class ExperimentControllerV2 { experimentService.toggleExperiment(experimentId, emailId); } + + @DeleteMapping("/reset-metrics") + @Timed(value = "experiment.reset.metric", percentiles = {0.95, 0.99}) + public void resetExperimentMetrics(@RequestParam("name") String experimentName + , @RequestHeader(Constants.HEADER_EMAIL_ID) String emailId) { + log.info("metric reset request received by: {} for experiment: {}", emailId, experimentName); + experimentService.resetMetrics(experimentName, emailId); + } } diff --git a/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentResultListener.java similarity index 52% rename from litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java rename to litmus-core/src/main/java/com/navi/medici/listener/ExperimentResultListener.java index 09d8213..4b3b8fb 100644 --- a/litmus-core/src/main/java/com/navi/medici/listener/ExperimentMetricResultListener.java +++ b/litmus-core/src/main/java/com/navi/medici/listener/ExperimentResultListener.java @@ -1,7 +1,13 @@ package com.navi.medici.listener; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.navi.medici.Constants; +import com.navi.medici.enums.QueryType; import com.navi.medici.metrics.MetricsUtils; import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import com.navi.medici.service.experimentpopulationresult.ExperimentPopulationResultService; +import com.navi.medici.util.JacksonUtils; import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -11,13 +17,17 @@ import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.stereotype.Component; +import java.util.Objects; + @Component @Log4j2 @RequiredArgsConstructor -public class ExperimentMetricResultListener { +public class ExperimentResultListener { private final ExperimentMetricResultService experimentMetricResultService; + private final ExperimentPopulationResultService experimentPopulationResultService; private final MeterRegistry meterRegistry; + private final JacksonUtils jacksonUtils; @KafkaListener(topics = "${kafka.experiment.metric.result.topic}", groupId = "${kafka.litmus.consumer.group}") public void listenMetricResultFromTesseract(@Header(value = KafkaHeaders.CORRELATION_ID, required = false) String correlationId, @@ -27,7 +37,9 @@ public class ExperimentMetricResultListener { ) { try { log.info("Payload received from tesseract, metric result: {} for tesseract_id: {} , correlation_id: {}", payload, tesseractId, correlationId); - experimentMetricResultService.persistMetricResultForExperiment(payload, tesseractId); + JsonNode responseJson = jacksonUtils.stringToObject(payload, JsonNode.class); + TextNode queryTypeJson = (TextNode) responseJson.findValue(Constants.QUERY_TYPE); + persistResultBasedOnQueryType(tesseractId, payload, queryTypeJson); } catch (Exception e) { log.error("Error while processing metric result for tesseract_id: " + tesseractId, e); MetricsUtils.tesseractMetrics("tesseract_metric_result_listener_failed") @@ -35,4 +47,22 @@ public class ExperimentMetricResultListener { .increment(); } } + + private void persistResultBasedOnQueryType(String tesseractId, String payload, TextNode queryTypeJson) { + if (Objects.nonNull(queryTypeJson)) { + QueryType queryType = QueryType.valueOf(queryTypeJson.textValue()); + switch (queryType) { + case METRIC: + experimentMetricResultService.persistMetricResultForExperiment(payload, tesseractId); + break; + case POPULATION: + experimentPopulationResultService.persistPopulationResultForExperiment(payload, tesseractId); + break; + default: + log.error("queryType: {} not found", queryType.name()); + } + } else { + log.error("queryType not found for tesseract received payload"); + } + } } diff --git a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java index bad4e1c..eb36b98 100644 --- a/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java +++ b/litmus-core/src/main/java/com/navi/medici/mapper/ExperimentMapper.java @@ -47,10 +47,9 @@ public class ExperimentMapper { .createdBy(experimentEntity.getCreatedBy()) .testUsers(testUsers) .progressPercent( - (Objects.nonNull(experimentEntity.getExperimentInfo()) - && experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired() > 0) + Math.min((Objects.nonNull(experimentEntity.getExperimentInfo()) && experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired() > 0) ? (double) testUsers / ((variants.size() > 0 ? variants.size() : 1) * experimentEntity.getExperimentInfo().getExperimentMetadata().getSampleSizeRequired()) - : 0 + : 0, 100) ) .primaryMetric(primaryMetric.map(MetricEntity::getMetricName).orElse(null)) .build(); diff --git a/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java deleted file mode 100644 index ee576d7..0000000 --- a/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentMetricResultScheduler.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.navi.medici.scheduler; - -import com.navi.medici.entity.ExperimentMetricMappingEntity; -import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; -import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; -import io.micrometer.core.instrument.Gauge; -import io.micrometer.core.instrument.MeterRegistry; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; -import net.javacrumbs.shedlock.core.SchedulerLock; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; - -import java.util.List; - -@Log4j2 -@Service -@RequiredArgsConstructor -public class ExperimentMetricResultScheduler { - - private final IExperimentMetricMappingQuery experimentMetricMappingQuery; - private final ExperimentMetricResultService experimentMetricResultService; - private final MeterRegistry meterRegistry; - - @Value("${experiment.metric.fetch.limit.per.interval}") - private int experimentMetricFetchLimitPerInterval; - - @Scheduled(cron = "${experiment.metric.fetch.cron}") - @SchedulerLock(name = "ExperimentMetricResultScheduler_fetchExperimentMetricResult", lockAtMostForString = "PT10S") - public void fetchExperimentMetricResult() { - log.info("Scheduling query registration"); - List experimentMetricMappings = experimentMetricMappingQuery.findByIsJobRunLimitTo(false, experimentMetricFetchLimitPerInterval); - experimentMetricResultService.registerExperimentMetricQueries(experimentMetricMappings); - Gauge.builder("queries_scheduled_per_interval", experimentMetricMappings, List::size) - .register(meterRegistry); - } - - @Scheduled(cron = "${experiment.metric.job.reset.cron}") - @SchedulerLock(name = "ExperimentMetricResultScheduler_resetExperimentMericJob", lockAtMostForString = "PT10S") - public void setExperimentMetricJobsToFalse() { - log.info("scheduling resetting of experiment metric jobs"); - experimentMetricResultService.setJobsToFalse(); - } -} diff --git a/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentResultScheduler.java b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentResultScheduler.java new file mode 100644 index 0000000..c18429e --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/scheduler/ExperimentResultScheduler.java @@ -0,0 +1,66 @@ +package com.navi.medici.scheduler; + +import com.navi.medici.config.LitmusCoreConfig; +import com.navi.medici.entity.ExperimentInfoEntity; +import com.navi.medici.entity.ExperimentMetricMappingEntity; +import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; +import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; +import com.navi.medici.service.experimentmetricresult.ExperimentMetricResultService; +import com.navi.medici.service.experimentpopulationresult.ExperimentPopulationResultService; +import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import net.javacrumbs.shedlock.core.SchedulerLock; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class ExperimentResultScheduler { + + private final IExperimentMetricMappingQuery experimentMetricMappingQuery; + private final IExperimentInfoQuery experimentInfoQuery; + private final ExperimentMetricResultService experimentMetricResultService; + private final ExperimentPopulationResultService experimentPopulationResultService; + private final MeterRegistry meterRegistry; + private final LitmusCoreConfig litmusCoreConfig; + + @Scheduled(cron = "${experiment.population.fetch.cron}") + @SchedulerLock(name = "ExperimentResultScheduler_fetchExperimentPopulationResult", lockAtMostForString = "PT10S") + public void fetchExperimentPopulationResult() { + log.info("Scheduling query registration for population"); + List experimentInfos = experimentInfoQuery.findByIsPopulationRunLimitTo(false, litmusCoreConfig.getExperimentPopulationFetchLimitPerInterval()); + experimentPopulationResultService.registerExperimentPopulationQueries(experimentInfos); + Gauge.builder("litmus_population_queries_scheduled_per_interval", experimentInfos, List::size) + .register(meterRegistry); + } + + @Scheduled(cron = "${experiment.metric.fetch.cron}") + @SchedulerLock(name = "ExperimentResultScheduler_fetchExperimentMetricResult", lockAtMostForString = "PT10S") + public void fetchExperimentMetricResult() { + log.info("Scheduling query registration for metrics"); + List experimentMetricMappings = experimentMetricMappingQuery.findByIsJobRunLimitTo(false, litmusCoreConfig.getExperimentMetricFetchLimitPerInterval()); + experimentMetricResultService.registerExperimentMetricQueries(experimentMetricMappings); + Gauge.builder("litmus_metric_queries_scheduled_per_interval", experimentMetricMappings, List::size) + .register(meterRegistry); + } + + @Scheduled(cron = "${experiment.metric.job.reset.cron}") + @SchedulerLock(name = "ExperimentResultScheduler_resetExperimentMericJob", lockAtMostForString = "PT10S") + public void setExperimentMetricJobsToFalse() { + log.info("scheduling resetting of experiment metric jobs"); + experimentMetricResultService.setJobsToFalse(); + } + + + @Scheduled(cron = "${experiment.population.job.reset.cron}") + @SchedulerLock(name = "ExperimentResultScheduler_resetExperimentPopulationJob", lockAtMostForString = "PT10S") + public void setExperimentPopulationJobsToFalse() { + log.info("scheduling resetting of experiment population jobs"); + experimentPopulationResultService.setPopulationJobsToFalse(); + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java index c91ec57..f6ed74f 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentService.java @@ -61,4 +61,6 @@ public interface ExperimentService { ExperimentDataDTO getExperimentStatsData(String experimentId); void toggleExperiment(String experimentId, String emailId); + + void resetMetrics(String experimentName, String emailId); } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java index df9b2ca..bbbd62f 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experiment/ExperimentServiceImpl.java @@ -14,6 +14,7 @@ import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.ExperimentMetricResultEntity; +import com.navi.medici.entity.ExperimentPopulationResultEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.TeamEntity; import com.navi.medici.enums.ExperimentMetricType; @@ -30,6 +31,7 @@ import com.navi.medici.query.experimentaudittrail.IExperimentAuditTrailQuery; import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; import com.navi.medici.query.experimentmetricmapping.IExperimentMetricMappingQuery; import com.navi.medici.query.experimentmetricresult.IExperimentMetricResultQuery; +import com.navi.medici.query.experimentpopulationresult.IExperimentPopulationResultQuery; import com.navi.medici.query.metric.IMetricQuery; import com.navi.medici.query.team.ITeamQuery; import com.navi.medici.request.v1.AttachMetricToExperimentRequest; @@ -53,7 +55,6 @@ import com.navi.medici.validator.ExperimentValidator; import com.navi.medici.variants.VariantDefinition; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; -import org.apache.commons.collections.map.HashedMap; import org.apache.commons.lang3.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -72,6 +73,8 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.UUID; import java.util.stream.Collectors; @@ -95,6 +98,7 @@ public class ExperimentServiceImpl implements ExperimentService { private final IExperimentInfoQuery experimentInfoQuery; private final IExperimentMetricResultQuery experimentMetricResultQuery; private final IExperimentMetricMappingQuery experimentMetricMappingQuery; + private final IExperimentPopulationResultQuery experimentPopulationResultQuery; @Override @Transactional @@ -222,10 +226,6 @@ public class ExperimentServiceImpl implements ExperimentService { } private ExperimentImpact getExperimentImpact(ExperimentEntity experimentEntity, Optional primaryMetric) { - List experimentMetricResults = new ArrayList<>(); - if (primaryMetric.isPresent()) { - experimentMetricResults.addAll(experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(experimentEntity, primaryMetric.get())); - } List variantNames = getVariantNamesForStats(experimentEntity); Map converted = new HashMap<>(); Map notConverted = new HashMap<>(); @@ -233,17 +233,17 @@ public class ExperimentServiceImpl implements ExperimentService { converted.put(variant, 0L); notConverted.put(variant, 0L); }); - experimentMetricResults.forEach(experimentMetricResult -> { - String variantName = experimentMetricResult.getVariantName(); - converted.put(variantName, converted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getConverted()); - notConverted.put(variantName, notConverted.get(experimentMetricResult.getVariantName()) + experimentMetricResult.getNotConverted()); - }); + primaryMetric.ifPresent(metric -> variantNames.forEach(variant -> { + List variantResults = experimentMetricResultQuery.findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(experimentEntity, metric, variant); + if (variantResults.size() > 0) { + converted.put(variant, variantResults.get(0).getConverted()); + notConverted.put(variant, variantResults.get(0).getNotConverted()); + } + })); ExperimentImpact experimentImpact = ExperimentImpact.builder() .control(converted.get(Constants.CONTROL) != 0 ? (double) (converted.get(Constants.CONTROL) * 100) / (converted.get(Constants.CONTROL) + notConverted.get(Constants.CONTROL)) : 0) - .treatment(0.0) - .variantName(Constants.TREATMENT) .build(); if (Objects.nonNull(experimentEntity.getExperimentInfo())) { variantNames.stream() @@ -577,7 +577,6 @@ public class ExperimentServiceImpl implements ExperimentService { @Override public ExperimentDataDTO getExperimentStatsData(String experimentId) { - ExperimentEntity experiment = getExperimentEntityFromId(experimentId); List variantNames = getVariantNamesForStats(experiment); List metricMappings = experimentMetricMappingQuery.findByExperiment(experiment); @@ -604,91 +603,77 @@ public class ExperimentServiceImpl implements ExperimentService { saveAuditTrail(experimentId, String.format("Experiment state changes. from: %s, to: %s", previousState, !previousState), emailId); } - private List> getPopulationGraphForExperiment(ExperimentEntity experiment, int populationGraphDataDaysInterval, List variantNames) { - Optional primaryMetricMapping = experiment.getExperimentMetricMappings().stream() - .filter(experimentMetricMapping -> ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.getExperimentMetricType())) - .findFirst(); - if (primaryMetricMapping.isEmpty()) { - log.error("Primary metric not present for expriment: {}", experiment.getName()); - return List.of(); - } - MetricEntity primaryMetric = primaryMetricMapping.get().getMetric(); - List experimentResults = experiment.getExperimentMetricResults().stream() - .filter(experimentMetricResult -> experimentMetricResult.getMetric().getId().equals(primaryMetric.getId())) - .sorted(Comparator.comparing(ExperimentMetricResultEntity::getTimestamp)) - .toList(); - - log.info("population graph data updated_after :{} , experiment results: {}", LocalDateTime.now().minusDays(populationGraphDataDaysInterval), experimentResults.toString()); - Map> variantPopulationValues = new HashMap<>(); - Map> variantPopulationTimestamps = new HashMap<>(); - populateGraphDataOfVariants(variantNames, experimentResults, variantPopulationValues, variantPopulationTimestamps); - List> populationGraph = new ArrayList<>(variantNames.stream() - .map(variantName -> GraphDTO.builder() - .name(variantName) - .values(variantPopulationValues.get(variantName)) - .timestamps(variantPopulationTimestamps.get(variantName)) - .build()) - .collect(Collectors.toList()) - ); - if (variantNames.size() > 2) { - return populationGraph.stream() - .filter(graphDTO -> !graphDTO.getName().equals(Constants.TREATMENT)) - .collect(Collectors.toList()); - } else { - return populationGraph; + @Override + @Transactional + public void resetMetrics(String experimentName, String emailId) { + Optional experimentOpt = experimentQuery.findByName(experimentName); + if (experimentOpt.isEmpty()) { + throw new LitmusExperimentNotFoundException("experiment with name " + experimentName + " not found"); } + ExperimentEntity experiment = experimentOpt.get(); + long rowsAffectedInMetricsTable = experimentMetricResultQuery.deleteByExperiment(experiment); + long rowsAffectedInPopulationTable = experimentPopulationResultQuery.deleteByExperiment(experiment); + experiment.getExperimentInfo().setTestUsers(0); + experimentQuery.save(experiment); + saveAuditTrail(experiment.getExperimentId(), Constants.METRICS_RESET, emailId); + log.info("rows affected in metrics table on resetting metrics for experiment:{} is = {}", experiment.getName(), rowsAffectedInMetricsTable); + log.info("rows affected in population table on resetting metrics for experiment:{} is = {}", experiment.getName(), rowsAffectedInPopulationTable); } - private void populateGraphDataOfVariants(List variantNames, List experimentResults, Map> variantPopulationValues, Map> variantPopulationTimestamps) { - variantNames.forEach(variantName -> { - variantPopulationValues.put(variantName, List.of()); - variantPopulationTimestamps.put(variantName, List.of()); + private List> getPopulationGraphForExperiment(ExperimentEntity experiment, int populationGraphDataDaysInterval, List variantNames) { + List> graph = new ArrayList<>(); + SortedSet timestamps = new TreeSet<>(); + LocalDateTime graphAfterDate = LocalDateTime.now().minusDays(populationGraphDataDaysInterval); + Map> timestampMap = new HashMap<>(); + variantNames.forEach(variant -> { + List variantPopulations = experimentPopulationResultQuery.findByExperimentAndVariantNameAndTimestampAfterOrderByTimestampAsc(experiment, variant, graphAfterDate); + variantPopulations.forEach(population -> { + Map updatedTimestampMap = timestampMap.containsKey(population.getTimestamp()) ? timestampMap.get(population.getTimestamp()) : new HashMap<>(); + updatedTimestampMap.put(variant, population.getTotalUsers()); + timestampMap.put(population.getTimestamp(), updatedTimestampMap); + timestamps.add(population.getTimestamp()); + }); }); - experimentResults.forEach(experimentResult -> { - String variantName = experimentResult.getVariantName(); - List population = new ArrayList<>(variantPopulationValues.get(variantName)); - List timestamp = new ArrayList<>(variantPopulationTimestamps.get(variantName)); - population.add(experimentResult.getTotalUsers()); - timestamp.add(DateTimeUtil.convertDateTimeToDefaultZone(experimentResult.getTimestamp())); - variantPopulationValues.put(variantName, population); - variantPopulationTimestamps.put(variantName, timestamp); + timestamps.forEach(timestamp -> { + variantNames.forEach(variant -> { + if (!timestampMap.get(timestamp).containsKey(variant)) { + Map updatedTimestampMap = timestampMap.containsKey(timestamp) ? timestampMap.get(timestamp) : new HashMap<>(); + updatedTimestampMap.put(variant, 0L); + timestampMap.put(timestamp, updatedTimestampMap); + } + }); }); + variantNames.forEach(variant -> { + GraphDTO variantGraph = GraphDTO.builder() + .name(variant) + .build(); + List populationPoints = new ArrayList<>(); + List timestampPoints = new ArrayList<>(); + timestamps.forEach(timestamp -> { + populationPoints.add(timestampMap.get(timestamp).get(variant)); + timestampPoints.add(DateTimeUtil.convertDateTimeToDefaultZone(timestamp)); + }); + variantGraph.setValues(populationPoints); + variantGraph.setTimestamps(timestampPoints); + graph.add(variantGraph); + }); + return graph; } private List getPopulationDoughnutForExperiment(ExperimentEntity experiment, List variantNames) { - Optional primaryMetricMapping = experiment.getExperimentMetricMappings().stream() - .filter(experimentMetricMapping -> ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.getExperimentMetricType())) - .findFirst(); - if (primaryMetricMapping.isEmpty()) { - log.error("Primary metric not present for expriment: {}", experiment.getName()); - return List.of(); - } - MetricEntity primaryMetric = primaryMetricMapping.get().getMetric(); - List experimentResults = experiment.getExperimentMetricResults().stream() - .filter(experimentMetricResult -> experimentMetricResult.getMetric().getId().equals(primaryMetric.getId())) - .toList(); - Map variantPopulation = new HashMap<>(); - variantNames.forEach(variantName -> { - variantPopulation.put(variantName, 0L); + List populationDoughnut = new ArrayList<>(); + variantNames.forEach(variant -> { + List variantPopulation = experimentPopulationResultQuery.findByExperimentAndVariantName(experiment, variant); + long variantVisitors = 0; + for (ExperimentPopulationResultEntity population : variantPopulation) { + variantVisitors += population.getTotalUsers(); + } + populationDoughnut.add(DoughnutSectionDTO.builder() + .key(variant) + .value(variantVisitors) + .build()); }); - experimentResults.forEach(experimentResult -> { - String variantName = experimentResult.getVariantName(); - variantPopulation.put(variantName, variantPopulation.get(variantName) + experimentResult.getTotalUsers()); - }); - List populationDoughnut = new ArrayList<>(variantNames.stream() - .map(variantName -> DoughnutSectionDTO.builder() - .key(variantName) - .value(variantPopulation.get(variantName)) - .build()) - .collect(Collectors.toList()) - ); - if (variantNames.size() > 2) { - return populationDoughnut.stream() - .filter(doughnut -> !doughnut.getKey().equals(Constants.TREATMENT)) - .collect(Collectors.toList()); - } else { - return populationDoughnut; - } + return populationDoughnut; } private List> getMetricStatsTableForExperiment(List variantNames, List metricMappings) { @@ -700,40 +685,31 @@ public class ExperimentServiceImpl implements ExperimentService { } private void addMetricDataToTable(List variantNames, List> table, ExperimentMetricMappingEntity metricMapping) { - List metricResults = experimentMetricResultQuery.findByExperimentAndMetricOrderByIdAsc(metricMapping.getExperiment(), metricMapping.getMetric()); - Map converted = new HashedMap(); - Map notConverted = new HashMap(); - variantNames.forEach(variantName -> converted.put(variantName, 0L)); - variantNames.forEach(variantName -> notConverted.put(variantName, 0L)); - metricResults.forEach(metricResult -> { - String variantName = metricResult.getVariantName(); - converted.put(variantName, converted.get(variantName) + metricResult.getConverted()); - notConverted.put(variantName, notConverted.get(variantName) + metricResult.getNotConverted()); - }); Map metricResult = new HashMap<>(); metricResult.put("metricName", metricMapping.getMetric().getMetricName()); metricResult.put("experimentMetricType", metricMapping.getExperimentMetricType()); - variantNames.forEach(variantName -> { - if (converted.get(variantName) == 0) { - metricResult.put(variantName, 0); + variantNames.forEach(variant -> { + List variantResults = experimentMetricResultQuery.findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(metricMapping.getExperiment(), metricMapping.getMetric(), variant); + if (variantResults.size() > 0) { + ExperimentMetricResultEntity latestVariantResult = variantResults.get(0); + metricResult.put(variant, (double) (latestVariantResult.getConverted() * 100) / (latestVariantResult.getConverted() + latestVariantResult.getNotConverted())); } else { - metricResult.put(variantName, (double) (converted.get(variantName) * 100) / (converted.get(variantName) + notConverted.get(variantName))); + metricResult.put(variant, 0); } }); - if (variantNames.size() > 2) { - metricResult.remove(Constants.TREATMENT); - } table.add(metricResult); } private List getVariantNamesForStats(ExperimentEntity experiment) { List variantNames = new ArrayList<>(); variantNames.add(Constants.CONTROL); - variantNames.add(Constants.TREATMENT); if (Objects.nonNull(experiment.getVariants()) && !Objects.equals(experiment.getVariants(), "null")) { List variants = jacksonUtils.stringToListObject(experiment.getVariants(), VariantDefinition.class); variants.forEach(variant -> variantNames.add(variant.getName())); } + if (variantNames.size() == 1) { + variantNames.add(Constants.TREATMENT); + } return variantNames; } } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java index b20f078..f9ee3b5 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/experimentmetricresult/ExperimentMetricResultServiceImpl.java @@ -4,12 +4,11 @@ import com.navi.medici.Constants; import com.navi.medici.config.LitmusCoreConfig; import com.navi.medici.dto.MetricResult; import com.navi.medici.entity.ExperimentEntity; -import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.entity.ExperimentMetricMappingEntity; import com.navi.medici.entity.ExperimentMetricResultEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.entity.TesseractIdStatusEntity; -import com.navi.medici.enums.ExperimentMetricType; +import com.navi.medici.enums.QueryType; import com.navi.medici.exceptions.LitmusExperimentNotFoundException; import com.navi.medici.exceptions.MetricNotFoundException; import com.navi.medici.metrics.MetricsUtils; @@ -24,6 +23,7 @@ import com.navi.medici.tesseract.response.TesseractIdStatusResponse; import com.navi.medici.tesseract.response.TesseractQueryRegisterResponse; import com.navi.medici.util.DateTimeUtil; import com.navi.medici.util.JacksonUtils; +import com.navi.medici.variants.VariantDefinition; import freemarker.template.TemplateException; import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; @@ -33,6 +33,7 @@ import org.springframework.stereotype.Service; import javax.transaction.Transactional; import java.io.IOException; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -67,18 +68,6 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult } ExperimentEntity experiment = experimentOpt.get(); ExperimentMetricResultEntity experimentMetricResult = buildExperimentMetricResultEntity(tesseractId, metricResult, metric, experiment); - List experimentMetricMapping = experimentMetricMappingQuery.findByExperimentAndMetric(experiment, metric.get()); - if (experimentMetricMapping.size() == 1 - && ExperimentMetricType.PRIMARY.equals(experimentMetricMapping.get(0).getExperimentMetricType())) { - ExperimentInfoEntity experimentInfo = experiment.getExperimentInfo(); - if (Objects.nonNull(experimentInfo)) { - experimentInfo.setTestUsers(experimentInfo.getTestUsers() + metricResult.getTotalUsers()); - experiment.setExperimentInfo(experimentInfo); - - log.info("saving experiments with updated test users: {}", experiment.getExperimentInfo().getTestUsers()); - experimentQuery.save(experiment); - } - } experimentMetricResultQuery.save(experimentMetricResult); } @@ -93,7 +82,6 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult .variantName(metricResult.getVariantName()) .converted(metricResult.getConverted()) .notConverted(metricResult.getNotConverted()) - .totalUsers(metricResult.getTotalUsers()) .timestamp(metricResult.getTimestamp()) .build(); } @@ -110,15 +98,18 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult if (Objects.nonNull(experimentMetricMapping.getQueryVariableMapping())) { variableMap = experimentMetricMapping.getQueryVariableMapping(); } - variableMap.putAll(getNecessaryVariableMappingForQuery(experiment, metric)); + variableMap.putAll(getNecessaryVariableMappingForMetricQuery(experiment, metric)); try { String processedQuery = tesseractQueryProcessingService.processMetricQuery(metric, variableMap); + String queryLog = processedQuery; + queryLog = queryLog.replaceAll("\\r\\n|\\r|\\n", " "); + log.info("experiment_name: {}, metric_name: {}, metric_query: {}", experiment.getName(), metric.getMetricName(), queryLog); TesseractQueryRegisterResponse tesseractQueryRegisterResponse = tesseractClient.register().apply(tesseractClient.getTesseractQueryRegisterRequest(processedQuery)); if (Objects.nonNull(tesseractQueryRegisterResponse)) { persistTesseractIdStatus(experiment, metric, tesseractQueryRegisterResponse); } else { log.error("Unable to register query for metric: {} mapped to experiment: {}", metric.getMetricName(), experiment.getName()); - MetricsUtils.tesseractMetrics("query_registration_failures") + MetricsUtils.tesseractMetrics("metric_query_registration_failures") .tag("experiment_name", experiment.getName()) .tag("metric_name", metric.getMetricName()) .register(meterRegistry) @@ -130,7 +121,7 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult .tag("metric_name", metric.getMetricName()) .register(meterRegistry) .increment(); - throw new RuntimeException(String.format("Failed to process metric query: %s", metricQuery), e); + throw new RuntimeException(String.format("Failed to process metric query: %s", metric.getAthenaQuery()), e); } }); } @@ -150,7 +141,7 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult private void setJobsToTrueForMappings(List experimentMetricMappings) { experimentMetricMappings.forEach(experimentMetricMapping -> { experimentMetricMapping.setJobRun(true); - log.info("saving experimentMetricMapping for id: {}", experimentMetricMapping.getId()); + log.info("setting jobRun to true for experimentMetricMapping for id: {}", experimentMetricMapping.getId()); experimentMetricMappingQuery.save(experimentMetricMapping); }); } @@ -189,10 +180,13 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult tesseractIdStatusQuery.save(tesseractIdStatus.get()); } - private Map getNecessaryVariableMappingForQuery(ExperimentEntity experiment, MetricEntity metric) { + private Map getNecessaryVariableMappingForMetricQuery(ExperimentEntity experiment, MetricEntity metric) { Map variableMap = new HashMap<>(); + variableMap.put(Constants.QUERY_TYPE, QueryType.METRIC.name()); variableMap.put(Constants.EXPERIMENT_NAME, experiment.getName()); variableMap.put(Constants.EXPERIMENT_ID, experiment.getId()); + variableMap.put(Constants.ARE_VARIANTS_PRESENT, "false"); + variableMap.put(Constants.VARIANT_NAMES, "[]"); variableMap.put(Constants.METRIC_ID, metric.getId()); variableMap.put(Constants.EXPERIMENT_CREATED_AT, DateTimeUtil.convertToSpacedISODateTime(experiment.getCreatedAt())); LocalDateTime scheduleTime = LocalDateTime.now(); @@ -207,6 +201,24 @@ public class ExperimentMetricResultServiceImpl implements ExperimentMetricResult .getTimestamp())); } variableMap.put(Constants.DAY_INTERVAL_FOR_TOTAL_USERS, litmusCoreConfig.getQueryDayIntervalForTotalUsers()); + + List variants = new ArrayList<>(); + if (Objects.nonNull(experiment.getVariants()) && !Objects.equals(experiment.getVariants(), "null")) { + variants = jacksonUtils.stringToListObject(experiment.getVariants(), VariantDefinition.class); + } + if (!variants.isEmpty()) { + variableMap.put(Constants.ARE_VARIANTS_PRESENT, "true"); + StringBuilder variantNamesAsString = new StringBuilder("["); + for (int i = 0; i < variants.size(); i++) { + variantNamesAsString.append(String.format("\"%s\"", variants.get(i).getName())); + if (i != variants.size() - 1) { + variantNamesAsString.append(","); + } + } + variantNamesAsString.append("]"); + variableMap.put(Constants.VARIANT_NAMES, variantNamesAsString); + } + return variableMap; } diff --git a/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultService.java b/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultService.java new file mode 100644 index 0000000..16b539c --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultService.java @@ -0,0 +1,15 @@ +package com.navi.medici.service.experimentpopulationresult; + +import com.navi.medici.entity.ExperimentInfoEntity; + +import java.util.List; + +public interface ExperimentPopulationResultService { + void registerExperimentPopulationQueries(List experimentInfos); + + void refreshPopulationResult(String experimentName); + + void persistPopulationResultForExperiment(String queryResponseFromTesseract, String tesseractId); + + void setPopulationJobsToFalse(); +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultServiceImpl.java new file mode 100644 index 0000000..a3ba02f --- /dev/null +++ b/litmus-core/src/main/java/com/navi/medici/service/experimentpopulationresult/ExperimentPopulationResultServiceImpl.java @@ -0,0 +1,221 @@ +package com.navi.medici.service.experimentpopulationresult; + +import com.navi.medici.Constants; +import com.navi.medici.config.LitmusCoreConfig; +import com.navi.medici.dto.PopulationResult; +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentInfoEntity; +import com.navi.medici.entity.ExperimentPopulationResultEntity; +import com.navi.medici.entity.MetricEntity; +import com.navi.medici.entity.TesseractIdStatusEntity; +import com.navi.medici.enums.QueryType; +import com.navi.medici.exceptions.LitmusExperimentNotFoundException; +import com.navi.medici.metrics.MetricsUtils; +import com.navi.medici.query.experiment.IExperimentQuery; +import com.navi.medici.query.experimentinfo.IExperimentInfoQuery; +import com.navi.medici.query.experimentpopulationresult.IExperimentPopulationResultQuery; +import com.navi.medici.query.metric.IMetricQuery; +import com.navi.medici.query.tesseractidstatus.ITesseractIdStatusQuery; +import com.navi.medici.service.queryprocessing.TesseractQueryProcessingService; +import com.navi.medici.tesseract.TesseractClient; +import com.navi.medici.tesseract.response.TesseractQueryRegisterResponse; +import com.navi.medici.util.DateTimeUtil; +import com.navi.medici.util.JacksonUtils; +import com.navi.medici.variants.VariantDefinition; +import freemarker.template.TemplateException; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import org.springframework.stereotype.Service; + +import javax.transaction.Transactional; +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class ExperimentPopulationResultServiceImpl implements ExperimentPopulationResultService { + private final IExperimentQuery experimentQuery; + private final IExperimentInfoQuery experimentInfoQuery; + private final IExperimentPopulationResultQuery experimentPopulationResultQuery; + private final TesseractQueryProcessingService tesseractQueryProcessingService; + private final JacksonUtils jacksonUtils; + private final LitmusCoreConfig litmusCoreConfig; + private final TesseractClient tesseractClient; + private final MeterRegistry meterRegistry; + private final ITesseractIdStatusQuery tesseractIdStatusQuery; + private final IMetricQuery metricQuery; + + @Override + public void registerExperimentPopulationQueries(List experimentInfos) { + setPopulationJobsToTrue(experimentInfos); + if (experimentInfos.size() > 0) { + experimentInfos.forEach(experimentInfo -> { + ExperimentEntity experiment = experimentInfo.getExperiment(); + log.info("registering query for population for experiment: {}", experiment.getName()); + try { + Map variableMap = getNecessaryVariableMappingForPopulationQuery(experiment); + Optional populationMetric = metricQuery.findByMetricName(Constants.POPULATION_METRIC); + if (populationMetric.isEmpty()) { + log.error("No population metric found"); + return; + } + String processedQuery = tesseractQueryProcessingService.processPopulationQuery(experiment, variableMap, populationMetric.get().getAthenaQuery()); + String queryLog = processedQuery; + queryLog = queryLog.replaceAll("\\r\\n|\\r|\\n", " "); + log.info("experiment_name: {},, population_query: {}", experiment.getName(), queryLog); + Optional metric = metricQuery.findByMetricName(Constants.POPULATION_METRIC); + TesseractQueryRegisterResponse tesseractQueryRegisterResponse = tesseractClient.register().apply(tesseractClient.getTesseractQueryRegisterRequest(processedQuery)); + if (Objects.nonNull(tesseractQueryRegisterResponse) && metric.isPresent()) { + persistTesseractIdStatus(experiment, metric.get(), tesseractQueryRegisterResponse); + } else { + log.error("Unable to register population query for experiment: {}", experiment.getName()); + MetricsUtils.tesseractMetrics("population_query_registration_failures") + .tag("experiment_name", experiment.getName()) + .register(meterRegistry) + .increment(); + } + } catch (TemplateException | IOException e) { + MetricsUtils.tesseractMetrics("population_query_resolve_failure") + .tag("experiment_name", experiment.getName()) + .register(meterRegistry) + .increment(); + throw new RuntimeException("Failed to process population query", e); + } + }); + } + } + + @Override + public void refreshPopulationResult(String experimentName) { + log.info("setting all population jobs to false for experiment: {}", experimentName); + ExperimentEntity experiment = getExperimentEntityFromName(experimentName); + Optional experimentInfo = experimentInfoQuery.findByExperiment(experiment); + experimentInfo.ifPresent(info -> { + info.setPopulationRun(false); + experimentInfoQuery.save(info); + }); + if (experimentInfo.isEmpty()) { + log.error("No info found for experiment: {}", experiment.getName()); + } + } + + @Override + @Transactional + public void persistPopulationResultForExperiment(String queryResponseFromTesseract, String tesseractId) { + PopulationResult populationResult = jacksonUtils.stringToObject(queryResponseFromTesseract, PopulationResult.class); + Optional experimentOpt = experimentQuery.findById(populationResult.getExperimentId()); + if (experimentOpt.isEmpty()) { + log.error("experiment with id: {} not present", populationResult.getExperimentId()); + return; + } + ExperimentEntity experiment = experimentOpt.get(); + ExperimentPopulationResultEntity experimentPopulationResult = buildExperimentPopulationResultEntity(tesseractId, populationResult, experiment); + if (!populationResult.getVariantName().equals(Constants.CONTROL)) { + ExperimentInfoEntity experimentInfo = experiment.getExperimentInfo(); + if (Objects.nonNull(experimentInfo)) { + experimentInfo.setTestUsers(experimentInfo.getTestUsers() + populationResult.getTotalUsers()); + experiment.setExperimentInfo(experimentInfo); + + log.info("saving experiments with updated test users: {}", experiment.getExperimentInfo().getTestUsers()); + experimentQuery.save(experiment); + } + } + experimentPopulationResultQuery.save(experimentPopulationResult); + } + + @Override + public void setPopulationJobsToFalse() { + log.info("setting all experiment population jobs to false that are true"); + List experimentInfos = experimentInfoQuery.findByPopulationRun(true); + experimentInfos.forEach(experimentInfo -> { + experimentInfo.setPopulationRun(false); + experimentInfoQuery.save(experimentInfo); + }); + } + + + private ExperimentPopulationResultEntity buildExperimentPopulationResultEntity(String tesseractId, PopulationResult populationResult, ExperimentEntity experiment) { + return ExperimentPopulationResultEntity.builder() + .experiment(experiment) + .variantName(populationResult.getVariantName()) + .totalUsers(populationResult.getTotalUsers()) + .tesseractId(tesseractId) + .timestamp(populationResult.getTimestamp()) + .build(); + } + + private void persistTesseractIdStatus(ExperimentEntity experiment, MetricEntity metric, TesseractQueryRegisterResponse tesseractQueryRegisterResponse) { + TesseractIdStatusEntity tesseractIdStatus = TesseractIdStatusEntity.builder() + .experiment(experiment) + .metric(metric) + .tesseractId(tesseractQueryRegisterResponse.getTesseractId()) + .build(); + tesseractClient.statusUpdate(tesseractQueryRegisterResponse.getTesseractId()) + .ifPresent(response -> tesseractIdStatus.setStatus(response.getStatus())); + tesseractIdStatusQuery.save(tesseractIdStatus); + } + + private void setPopulationJobsToTrue(List experimentInfos) { + experimentInfos.forEach(experimentInfo -> { + experimentInfo.setPopulationRun(true); + log.info("setting populationRun to true for experiment_id: {}", experimentInfo.getExperiment().getId()); + experimentInfoQuery.save(experimentInfo); + }); + } + + private Map getNecessaryVariableMappingForPopulationQuery(ExperimentEntity experiment) { + Map variableMap = new HashMap<>(); + LocalDateTime scheduleTime = LocalDateTime.now(); + variableMap.put(Constants.QUERY_TYPE, QueryType.POPULATION.name()); + variableMap.put(Constants.EXPERIMENT_NAME, experiment.getName()); + variableMap.put(Constants.EXPERIMENT_ID, experiment.getId()); + variableMap.put(Constants.ARE_VARIANTS_PRESENT, "false"); + variableMap.put(Constants.VARIANT_NAMES, "[]"); + variableMap.put(Constants.DAY_INTERVAL_FOR_TOTAL_USERS, litmusCoreConfig.getQueryDayIntervalForTotalUsers()); + variableMap.put(Constants.EXPERIMENT_CREATED_AT, DateTimeUtil.convertToSpacedISODateTime(experiment.getCreatedAt())); + variableMap.put(Constants.SCHEDULE_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime)); + variableMap.put(Constants.END_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime.minusHours(litmusCoreConfig.getQueryScheduleAndEndTimeDiff()))); + + List variants = new ArrayList<>(); + if (Objects.nonNull(experiment.getVariants()) && !Objects.equals(experiment.getVariants(), "null")) { + variants = jacksonUtils.stringToListObject(experiment.getVariants(), VariantDefinition.class); + } + if (!variants.isEmpty()) { + variableMap.put(Constants.ARE_VARIANTS_PRESENT, "true"); + StringBuilder variantNamesAsString = new StringBuilder("["); + for (int i = 0; i < variants.size(); i++) { + variantNamesAsString.append(String.format("\"%s\"", variants.get(i).getName())); + if (i != variants.size() - 1) { + variantNamesAsString.append(","); + } + } + variantNamesAsString.append("]"); + variableMap.put(Constants.VARIANT_NAMES, variantNamesAsString); + } + + List experimentPopulationResults = experimentPopulationResultQuery.findByExperimentOrderByTimestampAsc(experiment); + if (experimentPopulationResults.isEmpty()) { + variableMap.put(Constants.START_TIME, DateTimeUtil.convertToSpacedISODateTime(scheduleTime.minusDays(litmusCoreConfig.getPopulationGraphDataDaysInterval()))); + } else { + variableMap.put(Constants.START_TIME, DateTimeUtil.convertToSpacedISODateTime(experimentPopulationResults.get(experimentPopulationResults.size() - 1) + .getTimestamp())); + } + return variableMap; + } + + private ExperimentEntity getExperimentEntityFromName(String experimentName) { + Optional experimentEntity = experimentQuery.findByName(experimentName); + if (experimentEntity.isEmpty()) { + throw new LitmusExperimentNotFoundException("Experiment with name : " + experimentName + " is not present"); + } + return experimentEntity.get(); + } +} diff --git a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java index f9d5f7c..bd47e45 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/metric/MetricServiceImpl.java @@ -1,5 +1,6 @@ package com.navi.medici.service.metric; +import com.navi.medici.Constants; import com.navi.medici.dto.Dropdown; import com.navi.medici.entity.MetricEntity; import com.navi.medici.exceptions.MetricNotFoundException; @@ -86,6 +87,7 @@ public class MetricServiceImpl implements MetricService { public List getMetricsDropdown() { List metricNames = metricQuery.getAllMetricNames(); return metricNames.stream() + .filter(metricName -> !metricName.equals(Constants.POPULATION_METRIC)) .map(metricName -> Dropdown.builder() .label(metricName) .value(metricName) diff --git a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java index e09d5ae..25a5812 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java +++ b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingService.java @@ -1,5 +1,6 @@ package com.navi.medici.service.queryprocessing; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.MetricEntity; import freemarker.template.TemplateException; @@ -8,7 +9,9 @@ import java.util.List; import java.util.Map; public interface TesseractQueryProcessingService { - List getVariablesFromMetricQuery(MetricEntity metric) throws IOException; + List getVariablesFromMetricQuery(MetricEntity metric) throws TemplateException; String processMetricQuery(MetricEntity metric, Map variableMap) throws TemplateException, IOException; + + String processPopulationQuery(ExperimentEntity experiment, Map variableMap, String populationQuery) throws TemplateException, IOException; } diff --git a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java index 7b95f78..870bbe0 100644 --- a/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java +++ b/litmus-core/src/main/java/com/navi/medici/service/queryprocessing/TesseractQueryProcessingServiceImpl.java @@ -1,5 +1,6 @@ package com.navi.medici.service.queryprocessing; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.MetricEntity; import com.navi.medici.freemarker.FreeMarkerUtil; import lombok.SneakyThrows; @@ -24,4 +25,10 @@ public class TesseractQueryProcessingServiceImpl implements TesseractQueryProces return FreeMarkerUtil.processFreeMarkerMessage(metric.getMetricName(), metric.getMetricId(), metric.getAthenaQuery(), variableMap); } + @Override + @SneakyThrows + public String processPopulationQuery(ExperimentEntity experiment, Map variableMap, String populationQuery) { + return FreeMarkerUtil.processFreeMarkerMessage("population-metric", "population-metric", populationQuery, variableMap); + } + } diff --git a/litmus-core/src/main/resources/application.properties b/litmus-core/src/main/resources/application.properties index aad2ae1..4b81603 100644 --- a/litmus-core/src/main/resources/application.properties +++ b/litmus-core/src/main/resources/application.properties @@ -45,7 +45,7 @@ tesseract.update.path=${TESSERACT_UPDATE_PATH:/onboard/update/%s} tesseract.status.path=${TESSERACT_STATUS_PATH:/onboard/status/%s} tesseract.stats.path=${TESSERACT_STATS_PATH:/onboard/stats/%s} tesseract.litmus.cluster.name=${TESSERACT_LITMUS_CLUSTER_NAME:dev-kafka} -litmus.core.base.url=${LITMUS_CORE_BASE_URL:http://localhost:12000} +litmus.core.base.url=${LITMUS_CORE_BASE_URL:} tesseract.interval.function.initial.interval=${TESSERACT_INTERVAL_FUNCTION_INITIAL_INTERVAL:60000} tesseract.interval.function.max.interval=${TESSERACT_INTERVAL_FUNCTION_MAX_INTERVAL:100000} tesseract.interval.function.multiplier=${TESSERACT_INTERVAL_FUNCTION_MULTIPLIER:5} @@ -55,7 +55,10 @@ kafka.litmus.consumer.group=${KAFKA_LITMUS_CONSUMER_GROUP:dev-kafka} #metrics experiment.metric.fetch.cron=${EXPERIMENT_METRIC_FETCH_CRON:0 * * ? * *} experiment.metric.job.reset.cron=${EXPERIMENT_METRIC_JOB_RESET_CRON:0 58 23 ? * *} +experiment.population.fetch.cron=${EXPERIMENT_POPULATION_FETCH_CRON:0 * * ? * *} +experiment.population.job.reset.cron=${EXPERIMENT_POPULATION_JOB_RESET_CRON:0 58 23 ? * *} experiment.metric.fetch.limit.per.interval=${EXPERIMENT_METRIC_FETCH_LIMIT_PER_INTERVAL:5} +experiment.population.fetch.limit.per.interval=${EXPERIMENT_POPULATION_FETCH_LIMIT_PER_INTERVAL:5} population.graph.data.days.interval=${POPULATION_GRAPH_DATA_DAYS_INTERVAL:3} query.schedule.and.end.time.diff.hours=${QUERY_SCHEDULE_AND_END_TIME_DIFF_HOURS:2} query.day.interval.for.total.users=${QUERY_DAY_INTERVAL_FOR_TOTAL_USERS:30} \ No newline at end of file diff --git a/litmus-core/src/test/java/com/navi/medici/TestUtils.java b/litmus-core/src/test/java/com/navi/medici/TestUtils.java index cbcb040..f72ab63 100644 --- a/litmus-core/src/test/java/com/navi/medici/TestUtils.java +++ b/litmus-core/src/test/java/com/navi/medici/TestUtils.java @@ -142,7 +142,6 @@ public class TestUtils { public static ExperimentMetricResultEntity getExperimentMetricResultEntity() { return ExperimentMetricResultEntity.builder() .timestamp(getLocalDateTime()) - .totalUsers(0) .metric(getMetricEntity()) .experiment(getExperimentEntity()) .converted(0) diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java index 9bd64f8..c5309ce 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentEntity.java @@ -82,6 +82,10 @@ public class ExperimentEntity extends BaseEntity { @Cascade(CascadeType.ALL) Set experimentMetricResults; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") + @Cascade(CascadeType.ALL) + Set experimentPopulationResults; + @OneToMany(fetch = FetchType.LAZY, mappedBy = "experiment") @Cascade(CascadeType.ALL) Set experimentAuditTrails; diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentInfoEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentInfoEntity.java index c4ae4da..d2844d6 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentInfoEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentInfoEntity.java @@ -40,6 +40,7 @@ public class ExperimentInfoEntity extends BaseEntity { ExperimentStatus experimentStatus; long testUsers; + boolean isPopulationRun; @ManyToOne @JoinColumn(name = "team_id") diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java index 18dd9ec..6b3bf89 100644 --- a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentMetricResultEntity.java @@ -8,8 +8,6 @@ import lombok.Setter; import lombok.ToString; import lombok.experimental.FieldDefaults; import lombok.experimental.SuperBuilder; -import org.hibernate.annotations.Cascade; -import org.hibernate.annotations.CascadeType; import javax.persistence.Entity; import javax.persistence.JoinColumn; @@ -29,12 +27,10 @@ import java.time.LocalDateTime; public class ExperimentMetricResultEntity extends BaseEntity { @ManyToOne @JoinColumn(name = "experiment_id") - @Cascade(CascadeType.ALL) ExperimentEntity experiment; @ManyToOne @JoinColumn(name = "metric_id") - @Cascade(CascadeType.ALL) MetricEntity metric; String tesseractId; @@ -42,6 +38,5 @@ public class ExperimentMetricResultEntity extends BaseEntity { String variantName; long converted; long notConverted; - long totalUsers; LocalDateTime timestamp; } diff --git a/litmus-db/src/main/java/com/navi/medici/entity/ExperimentPopulationResultEntity.java b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentPopulationResultEntity.java new file mode 100644 index 0000000..68b7cde --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/entity/ExperimentPopulationResultEntity.java @@ -0,0 +1,36 @@ +package com.navi.medici.entity; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.FieldDefaults; +import lombok.experimental.SuperBuilder; + +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; +import java.time.LocalDateTime; + +@Entity +@Table(name = "experiment_population_result") +@Getter +@Setter +@SuperBuilder +@ToString +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = AccessLevel.PRIVATE) +public class ExperimentPopulationResultEntity extends BaseEntity { + @ManyToOne + @JoinColumn(name = "experiment_id") + ExperimentEntity experiment; + + String variantName; + long totalUsers; + String tesseractId; + LocalDateTime timestamp; +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/ExperimentInfoQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/ExperimentInfoQueryImpl.java index e36f735..619b4f7 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/ExperimentInfoQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/ExperimentInfoQueryImpl.java @@ -1,10 +1,14 @@ package com.navi.medici.query.experimentinfo; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; import com.navi.medici.repository.ExperimentInfoRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Optional; + @Component @RequiredArgsConstructor public class ExperimentInfoQueryImpl implements IExperimentInfoQuery { @@ -15,4 +19,19 @@ public class ExperimentInfoQueryImpl implements IExperimentInfoQuery { public ExperimentInfoEntity save(ExperimentInfoEntity experimentInfo) { return experimentInfoRepository.save(experimentInfo); } + + @Override + public List findByIsPopulationRunLimitTo(boolean isPopulationRun, int experimentPopulationFetchLimitPerInterval) { + return experimentInfoRepository.findByIsPopulationRunLimitTo(isPopulationRun, experimentPopulationFetchLimitPerInterval); + } + + @Override + public Optional findByExperiment(ExperimentEntity experiment) { + return experimentInfoRepository.findByExperiment(experiment); + } + + @Override + public List findByPopulationRun(boolean isPopulationRun) { + return experimentInfoRepository.findByPopulationRun(isPopulationRun); + } } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/IExperimentInfoQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/IExperimentInfoQuery.java index 1f4a524..4975b53 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/IExperimentInfoQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentinfo/IExperimentInfoQuery.java @@ -1,7 +1,18 @@ package com.navi.medici.query.experimentinfo; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; +import java.util.List; +import java.util.Optional; + public interface IExperimentInfoQuery { ExperimentInfoEntity save(ExperimentInfoEntity experimentInfo); + + List findByIsPopulationRunLimitTo(boolean isPopulationRun, int experimentPopulationFetchLimitPerInterval); + + Optional findByExperiment(ExperimentEntity experiment); + + List findByPopulationRun(boolean isPopulationRun); + } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java index d4a273d..574f331 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/ExperimentMetricResultQueryImpl.java @@ -48,4 +48,14 @@ public class ExperimentMetricResultQueryImpl implements IExperimentMetricResultQ return experimentMetricResultRepository.findByExperimentAndUpdatedAtAfter(experiment, updatedAfter); } + @Override + public List findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(ExperimentEntity experiment, MetricEntity metric, String variantName) { + return experimentMetricResultRepository.findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(experiment, metric, variantName); + } + + @Override + public long deleteByExperiment(ExperimentEntity experiment) { + return experimentMetricResultRepository.deleteByExperiment(experiment); + } + } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java index cc2970d..83918f2 100644 --- a/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentmetricresult/IExperimentMetricResultQuery.java @@ -20,4 +20,8 @@ public interface IExperimentMetricResultQuery { List findByExperimentAndMetricOrderByTimestampAsc(ExperimentEntity experiment, MetricEntity metric); List findByExperimentUpdatedAfter(ExperimentEntity experiment, LocalDateTime updatedAfter); + + List findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(ExperimentEntity experiment, MetricEntity metric, String variantName); + + long deleteByExperiment(ExperimentEntity experiment); } diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/ExperimentPopulationResultQueryImpl.java b/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/ExperimentPopulationResultQueryImpl.java new file mode 100644 index 0000000..f73d6bc --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/ExperimentPopulationResultQueryImpl.java @@ -0,0 +1,41 @@ +package com.navi.medici.query.experimentpopulationresult; + +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentPopulationResultEntity; +import com.navi.medici.repository.ExperimentPopulationResultRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class ExperimentPopulationResultQueryImpl implements IExperimentPopulationResultQuery { + private final ExperimentPopulationResultRepository experimentPopulationResultRepository; + + @Override + public List findByExperimentOrderByTimestampAsc(ExperimentEntity experiment) { + return experimentPopulationResultRepository.findByExperimentOrderByTimestampAsc(experiment); + } + + @Override + public ExperimentPopulationResultEntity save(ExperimentPopulationResultEntity experimentPopulationResult) { + return experimentPopulationResultRepository.save(experimentPopulationResult); + } + + @Override + public List findByExperimentAndVariantNameAndTimestampAfterOrderByTimestampAsc(ExperimentEntity experiment, String variantName, LocalDateTime timestamp) { + return experimentPopulationResultRepository.findByExperimentAndVariantNameAndTimestampAfterOrderByTimestampAsc(experiment, variantName, timestamp); + } + + @Override + public List findByExperimentAndVariantName(ExperimentEntity experiment, String variantName) { + return experimentPopulationResultRepository.findByExperimentAndVariantName(experiment, variantName); + } + + @Override + public long deleteByExperiment(ExperimentEntity experiment) { + return experimentPopulationResultRepository.deleteByExperiment(experiment); + } +} diff --git a/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/IExperimentPopulationResultQuery.java b/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/IExperimentPopulationResultQuery.java new file mode 100644 index 0000000..0f260a9 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/query/experimentpopulationresult/IExperimentPopulationResultQuery.java @@ -0,0 +1,19 @@ +package com.navi.medici.query.experimentpopulationresult; + +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentPopulationResultEntity; + +import java.time.LocalDateTime; +import java.util.List; + +public interface IExperimentPopulationResultQuery { + List findByExperimentOrderByTimestampAsc(ExperimentEntity experiment); + + ExperimentPopulationResultEntity save(ExperimentPopulationResultEntity experimentPopulationResult); + + List findByExperimentAndVariantNameAndTimestampAfterOrderByTimestampAsc(ExperimentEntity experiment, String variantName, LocalDateTime timestamp); + + List findByExperimentAndVariantName(ExperimentEntity experiment, String variantName); + + long deleteByExperiment(ExperimentEntity experiment); +} diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentInfoRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentInfoRepository.java index 9dece75..2448f50 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentInfoRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentInfoRepository.java @@ -1,9 +1,21 @@ package com.navi.medici.repository; +import com.navi.medici.entity.ExperimentEntity; import com.navi.medici.entity.ExperimentInfoEntity; +import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; + @Repository public interface ExperimentInfoRepository extends CrudRepository { + @Query(value = "select * from experiment_info ei where ei.is_population_run=:isPopulationRun and ei.experiment_status = 'RUNNING' order by ei.updated_at asc limit :experimentPopulationFetchLimitPerInterval", nativeQuery = true) + List findByIsPopulationRunLimitTo(boolean isPopulationRun, int experimentPopulationFetchLimitPerInterval); + + Optional findByExperiment(ExperimentEntity experiment); + + @Query(value = "select * from experiment_info ei where ei.is_population_run=:isPopulationRun", nativeQuery = true) + List findByPopulationRun(boolean isPopulationRun); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java index 4c15960..89970ae 100644 --- a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentMetricResultRepository.java @@ -21,4 +21,8 @@ public interface ExperimentMetricResultRepository extends CrudRepository findByExperimentAndMetricOrderByTimestampAsc(ExperimentEntity experiment, MetricEntity metric); List findByExperimentAndUpdatedAtAfter(ExperimentEntity experiment, LocalDateTime updatedAfter); + + List findByExperimentAndMetricAndVariantNameOrderByTimestampDesc(ExperimentEntity experiment, MetricEntity metric, String variantName); + + long deleteByExperiment(ExperimentEntity experiment); } diff --git a/litmus-db/src/main/java/com/navi/medici/repository/ExperimentPopulationResultRepository.java b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentPopulationResultRepository.java new file mode 100644 index 0000000..34b7fe2 --- /dev/null +++ b/litmus-db/src/main/java/com/navi/medici/repository/ExperimentPopulationResultRepository.java @@ -0,0 +1,18 @@ +package com.navi.medici.repository; + +import com.navi.medici.entity.ExperimentEntity; +import com.navi.medici.entity.ExperimentPopulationResultEntity; +import org.springframework.data.repository.CrudRepository; + +import java.time.LocalDateTime; +import java.util.List; + +public interface ExperimentPopulationResultRepository extends CrudRepository { + List findByExperimentOrderByTimestampAsc(ExperimentEntity experiment); + + List findByExperimentAndVariantNameAndTimestampAfterOrderByTimestampAsc(ExperimentEntity experiment, String variantName, LocalDateTime timestamp); + + List findByExperimentAndVariantName(ExperimentEntity experiment, String variantName); + + long deleteByExperiment(ExperimentEntity experiment); +} diff --git a/litmus-liquibase/src/main/resources/db/changelog/202305121939-create-population-table.sql b/litmus-liquibase/src/main/resources/db/changelog/202305121939-create-population-table.sql new file mode 100644 index 0000000..942a22b --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202305121939-create-population-table.sql @@ -0,0 +1,23 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202305121939 +ALTER TABLE experiment_metric_result + DROP COLUMN total_users; + +CREATE TABLE experiment_population_result +( + id SERIAL PRIMARY KEY, + experiment_id int references experiments (id), + variant_name varchar(150), + total_users bigint, + timestamp timestamp, + tesseract_id varchar(36), + created_at timestamp, + updated_at timestamp, + version bigint +); + +CREATE INDEX idx_metric_result_variant_name on experiment_metric_result (variant_name); +CREATE INDEX idx_metric_result_timestamp on experiment_metric_result (timestamp); +CREATE INDEX idx_population_result_variant_name on experiment_population_result (variant_name); +CREATE INDEX idx_population_result_timestamp on experiment_population_result (timestamp); \ No newline at end of file diff --git a/litmus-liquibase/src/main/resources/db/changelog/202305122024-add-is-population-run-in-exp-info.sql b/litmus-liquibase/src/main/resources/db/changelog/202305122024-add-is-population-run-in-exp-info.sql new file mode 100644 index 0000000..9728c84 --- /dev/null +++ b/litmus-liquibase/src/main/resources/db/changelog/202305122024-add-is-population-run-in-exp-info.sql @@ -0,0 +1,5 @@ +--liquibase formatted sql + +--changeset author:akshatsoni id:202305122024 +ALTER TABLE experiment_info + ADD COLUMN is_population_run boolean DEFAULT false; \ No newline at end of file diff --git a/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java b/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java index c3cb446..1253085 100644 --- a/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java +++ b/litmus-model/src/main/java/com/navi/medici/dto/MetricResult.java @@ -1,5 +1,6 @@ package com.navi.medici.dto; +import com.navi.medici.enums.QueryType; import lombok.AccessLevel; import lombok.Data; import lombok.experimental.FieldDefaults; @@ -9,11 +10,11 @@ import java.time.LocalDateTime; @Data @FieldDefaults(level = AccessLevel.PRIVATE) public class MetricResult { + QueryType queryType; long experimentId; long metricId; String variantName; long converted; long notConverted; - long totalUsers; LocalDateTime timestamp; } diff --git a/litmus-model/src/main/java/com/navi/medici/dto/PopulationResult.java b/litmus-model/src/main/java/com/navi/medici/dto/PopulationResult.java new file mode 100644 index 0000000..384ffbd --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/dto/PopulationResult.java @@ -0,0 +1,18 @@ +package com.navi.medici.dto; + +import com.navi.medici.enums.QueryType; +import lombok.AccessLevel; +import lombok.Data; +import lombok.experimental.FieldDefaults; + +import java.time.LocalDateTime; + +@Data +@FieldDefaults(level = AccessLevel.PRIVATE) +public class PopulationResult { + QueryType queryType; + long experimentId; + String variantName; + long totalUsers; + LocalDateTime timestamp; +} \ No newline at end of file diff --git a/litmus-model/src/main/java/com/navi/medici/enums/QueryType.java b/litmus-model/src/main/java/com/navi/medici/enums/QueryType.java new file mode 100644 index 0000000..0e9f3fd --- /dev/null +++ b/litmus-model/src/main/java/com/navi/medici/enums/QueryType.java @@ -0,0 +1,6 @@ +package com.navi.medici.enums; + +public enum QueryType { + METRIC, + POPULATION +}