/** * Calculates the how often a canary judgement should be performed during the lifetime of the canary analysis execution. * * @param canaryAnalysisExecutionRequest The execution requests * @param lifetime The calculated lifetime of the canary analysis execution * @return How often a judgement should be performed */ protected Duration calculateAnalysisInterval(CanaryAnalysisExecutionRequest canaryAnalysisExecutionRequest, Duration lifetime) { Duration analysisInterval; if (canaryAnalysisExecutionRequest.getAnalysisIntervalMins() != null) { analysisInterval = Duration.ofMinutes(canaryAnalysisExecutionRequest.getAnalysisIntervalMins()); } else { analysisInterval = lifetime; } if (analysisInterval == ZERO || Instant.ofEpochMilli(analysisInterval.toMillis()).isAfter(Instant.ofEpochMilli(lifetime.toMillis()))) { analysisInterval = lifetime; } return analysisInterval; }
/** * Calculates the start and end timestamps that will be used when quering the metrics sources when doing the * canary judgements for each judgement interval. * * @param judgementNumber The judgement number / index for the canary analysis execution * @param judgementDuration The duration of the judgement window * @param config The execution request config * @return A wrapper object containing the start and end times to be used as Instants */ protected ScopeTimeConfig calculateStartAndEndForJudgement(CanaryAnalysisExecutionRequest config, long judgementNumber, Duration judgementDuration) { Duration warmupDuration = config.getBeginCanaryAnalysisAfterAsDuration(); Duration offset = judgementDuration.multipliedBy(judgementNumber); ScopeTimeConfig scopeTimeConfig = new ScopeTimeConfig(); Instant startTime = Optional.ofNullable(config.getStartTime()).orElse(now(clock)); scopeTimeConfig.start = startTime; scopeTimeConfig.end = startTime.plus(offset); if (config.getEndTime() == null) { scopeTimeConfig.start = scopeTimeConfig.start.plus(warmupDuration); scopeTimeConfig.end = scopeTimeConfig.end.plus(warmupDuration); } // If the look back is defined, use it to recalculate the start time, this is used to do sliding window judgements if (config.getLookBackAsInstant().isAfter(ZERO_AS_INSTANT)) { scopeTimeConfig.start = scopeTimeConfig.end.minus(config.getLookBackAsDuration()); } return scopeTimeConfig; }
/** * Calculates the lifetime duration for the canary analysis execution. * * @param start The calculated start time for the execution * @param endTime The calculated endtime * @param canaryAnalysisExecutionRequest The execution request * @return The calculated duration of the canary analysis */ protected Duration calculateLifetime(Instant start, Instant endTime, CanaryAnalysisExecutionRequest canaryAnalysisExecutionRequest) { Duration lifetime; if (endTime != null) { lifetime = Duration.ofMinutes(start.until(endTime, MINUTES)); } else if (canaryAnalysisExecutionRequest.getLifetimeDuration() != null) { lifetime = canaryAnalysisExecutionRequest.getLifetimeDuration(); } else { throw new IllegalArgumentException("Canary stage configuration must include either `endTime` or `lifetimeDuration`."); } return lifetime; }
if (canaryAnalysisExecutionRequest.getScopes().isEmpty()) { throw new IllegalArgumentException("Canary stage configuration must contain at least one scope."); Instant start = Optional.ofNullable(canaryAnalysisExecutionRequest.getStartTime()).orElse(now(clock)); Instant endTime = canaryAnalysisExecutionRequest.getEndTime(); Duration lifetime = calculateLifetime(start, endTime, canaryAnalysisExecutionRequest); Duration analysisInterval = calculateAnalysisInterval(canaryAnalysisExecutionRequest, lifetime); if (canaryAnalysisExecutionRequest.getBeginCanaryAnalysisAfterAsInstant().isAfter(ZERO_AS_INSTANT)) { graph.append(stage -> { stage.setType(WaitStage.STAGE_TYPE); stage.setName("Warmup Wait"); stage.getContext().put("waitTime", canaryAnalysisExecutionRequest.getBeginCanaryAnalysisAfterAsDuration().getSeconds()); }); .canaryConfig(canaryAnalysisConfig.getCanaryConfig()) .scopes(buildRequestScopes(canaryAnalysisExecutionRequest, i, analysisInterval)) .scoreThresholds(canaryAnalysisExecutionRequest.getThresholds()) .siteLocal(canaryAnalysisExecutionRequest.getSiteLocal()) .build();
protected Map<String, CanaryScopePair> buildRequestScopes(CanaryAnalysisExecutionRequest config, long interval, Duration intervalDuration) { Map<String, CanaryScopePair> scopes = new HashMap<>(); config.getScopes().forEach(scope -> { ScopeTimeConfig scopeTimeConfig = calculateStartAndEndForJudgement(config, interval, intervalDuration); CanaryScope controlScope = new CanaryScope( scope.getControlScope(), scope.getControlLocation(), scopeTimeConfig.start, scopeTimeConfig.end, config.getStep().getSeconds(), scope.getExtendedScopeParams() ); CanaryScope experimentScope = new CanaryScope( scope.getExperimentScope(), scope.getExperimentLocation(), scopeTimeConfig.start, scopeTimeConfig.end, config.getStep().getSeconds(), scope.getExtendedScopeParams() ); CanaryScopePair canaryScopePair = new CanaryScopePair() .setControlScope(controlScope) .setExperimentScope(experimentScope); scopes.put(scope.getScopeName(), canaryScopePair); }); return scopes; }
@JsonIgnore public Instant getLookBackAsInstant() { return Instant.ofEpochMilli(getLookBackAsDuration().toMillis()); } }
@JsonIgnore public Instant getBeginCanaryAnalysisAfterAsInstant() { return Instant.ofEpochMilli(getBeginCanaryAnalysisAfterAsDuration().toMillis()); }
if (canaryAnalysisExecutionRequest.getScopes().isEmpty()) { throw new IllegalArgumentException("Canary stage configuration must contain at least one scope."); Instant start = Optional.ofNullable(canaryAnalysisExecutionRequest.getStartTime()).orElse(now(clock)); Instant endTime = canaryAnalysisExecutionRequest.getEndTime(); Duration lifetime = calculateLifetime(start, endTime, canaryAnalysisExecutionRequest); Duration analysisInterval = calculateAnalysisInterval(canaryAnalysisExecutionRequest, lifetime); if (canaryAnalysisExecutionRequest.getBeginCanaryAnalysisAfterAsInstant().isAfter(ZERO_AS_INSTANT)) { graph.append(stage -> { stage.setType(WaitStage.STAGE_TYPE); stage.setName("Warmup Wait"); stage.getContext().put("waitTime", canaryAnalysisExecutionRequest.getBeginCanaryAnalysisAfterAsDuration().getSeconds()); }); .canaryConfig(canaryAnalysisConfig.getCanaryConfig()) .scopes(buildRequestScopes(canaryAnalysisExecutionRequest, i, analysisInterval)) .scoreThresholds(canaryAnalysisExecutionRequest.getThresholds()) .siteLocal(canaryAnalysisExecutionRequest.getSiteLocal()) .build();
protected Map<String, CanaryScopePair> buildRequestScopes(CanaryAnalysisExecutionRequest config, long interval, Duration intervalDuration) { Map<String, CanaryScopePair> scopes = new HashMap<>(); config.getScopes().forEach(scope -> { ScopeTimeConfig scopeTimeConfig = calculateStartAndEndForJudgement(config, interval, intervalDuration); CanaryScope controlScope = new CanaryScope( scope.getControlScope(), scope.getControlLocation(), scopeTimeConfig.start, scopeTimeConfig.end, config.getStep().getSeconds(), scope.getExtendedScopeParams() ); CanaryScope experimentScope = new CanaryScope( scope.getExperimentScope(), scope.getExperimentLocation(), scopeTimeConfig.start, scopeTimeConfig.end, config.getStep().getSeconds(), scope.getExtendedScopeParams() ); CanaryScopePair canaryScopePair = new CanaryScopePair() .setControlScope(controlScope) .setExperimentScope(experimentScope); scopes.put(scope.getScopeName(), canaryScopePair); }); return scopes; }
@JsonIgnore public Instant getLookBackAsInstant() { return Instant.ofEpochMilli(getLookBackAsDuration().toMillis()); } }
@JsonIgnore public Instant getBeginCanaryAnalysisAfterAsInstant() { return Instant.ofEpochMilli(getBeginCanaryAnalysisAfterAsDuration().toMillis()); }
/** * Calculates the start and end timestamps that will be used when quering the metrics sources when doing the * canary judgements for each judgement interval. * * @param judgementNumber The judgement number / index for the canary analysis execution * @param judgementDuration The duration of the judgement window * @param config The execution request config * @return A wrapper object containing the start and end times to be used as Instants */ protected ScopeTimeConfig calculateStartAndEndForJudgement(CanaryAnalysisExecutionRequest config, long judgementNumber, Duration judgementDuration) { Duration warmupDuration = config.getBeginCanaryAnalysisAfterAsDuration(); Duration offset = judgementDuration.multipliedBy(judgementNumber); ScopeTimeConfig scopeTimeConfig = new ScopeTimeConfig(); Instant startTime = Optional.ofNullable(config.getStartTime()).orElse(now(clock)); scopeTimeConfig.start = startTime; scopeTimeConfig.end = startTime.plus(offset); if (config.getEndTime() == null) { scopeTimeConfig.start = scopeTimeConfig.start.plus(warmupDuration); scopeTimeConfig.end = scopeTimeConfig.end.plus(warmupDuration); } // If the look back is defined, use it to recalculate the start time, this is used to do sliding window judgements if (config.getLookBackAsInstant().isAfter(ZERO_AS_INSTANT)) { scopeTimeConfig.start = scopeTimeConfig.end.minus(config.getLookBackAsDuration()); } return scopeTimeConfig; }
/** * Calculates the how often a canary judgement should be performed during the lifetime of the canary analysis execution. * * @param canaryAnalysisExecutionRequest The execution requests * @param lifetime The calculated lifetime of the canary analysis execution * @return How often a judgement should be performed */ protected Duration calculateAnalysisInterval(CanaryAnalysisExecutionRequest canaryAnalysisExecutionRequest, Duration lifetime) { Duration analysisInterval; if (canaryAnalysisExecutionRequest.getAnalysisIntervalMins() != null) { analysisInterval = Duration.ofMinutes(canaryAnalysisExecutionRequest.getAnalysisIntervalMins()); } else { analysisInterval = lifetime; } if (analysisInterval == ZERO || Instant.ofEpochMilli(analysisInterval.toMillis()).isAfter(Instant.ofEpochMilli(lifetime.toMillis()))) { analysisInterval = lifetime; } return analysisInterval; }
/** * Calculates the lifetime duration for the canary analysis execution. * * @param start The calculated start time for the execution * @param endTime The calculated endtime * @param canaryAnalysisExecutionRequest The execution request * @return The calculated duration of the canary analysis */ protected Duration calculateLifetime(Instant start, Instant endTime, CanaryAnalysisExecutionRequest canaryAnalysisExecutionRequest) { Duration lifetime; if (endTime != null) { lifetime = Duration.ofMinutes(start.until(endTime, MINUTES)); } else if (canaryAnalysisExecutionRequest.getLifetimeDuration() != null) { lifetime = canaryAnalysisExecutionRequest.getLifetimeDuration(); } else { throw new IllegalArgumentException("Canary stage configuration must include either `endTime` or `lifetimeDuration`."); } return lifetime; }