com.netflix.spinnaker.echo.pipelinetriggers.PipelineCache Maven / Gradle / Ivy
The newest version!
/*
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.netflix.spinnaker.echo.pipelinetriggers;
import static java.time.Instant.now;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.netflix.spectator.api.Registry;
import com.netflix.spectator.api.patterns.PolledMeter;
import com.netflix.spinnaker.echo.model.Pipeline;
import com.netflix.spinnaker.echo.model.Trigger;
import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.BaseTriggerEventHandler;
import com.netflix.spinnaker.echo.pipelinetriggers.eventhandlers.TriggerEventHandler;
import com.netflix.spinnaker.echo.pipelinetriggers.orca.OrcaService;
import com.netflix.spinnaker.echo.services.Front50Service;
import com.netflix.spinnaker.security.AuthenticatedRequest;
import java.time.Duration;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class PipelineCache implements MonitoredPoller {
private final int pollingIntervalMs;
private final int pollingSleepMs;
private final Front50Service front50;
private final OrcaService orca;
private final Registry registry;
private final ScheduledExecutorService executorService;
private final ObjectMapper objectMapper;
private final PipelineCacheConfigurationProperties pipelineCacheConfigurationProperties;
/**
* If enabled by a feature flag, query front50 for (enabled) pipelines with only these (enabled)
* trigger types. A comma-separated string since that's what front50 expects. Getter only for
* testing.
*/
@Getter private final String supportedTriggerTypes;
private volatile Boolean running;
private volatile Instant lastPollTimestamp;
@Nullable private volatile List pipelines;
@Nullable private volatile Map> triggersByType;
@Autowired
public PipelineCache(
@Value("${front50.polling-interval-ms:30000}") int pollingIntervalMs,
@Value("${front50.polling-sleep-ms:100}") int pollingSleepMs,
@NonNull PipelineCacheConfigurationProperties pipelineCacheConfigurationProperties,
ObjectMapper objectMapper,
@NonNull Front50Service front50,
@NonNull OrcaService orca,
@NonNull Registry registry,
@NonNull List triggerHandlers) {
this(
Executors.newSingleThreadScheduledExecutor(),
pollingIntervalMs,
pollingSleepMs,
pipelineCacheConfigurationProperties,
objectMapper,
front50,
orca,
registry,
triggerHandlers);
}
// VisibleForTesting
public PipelineCache(
ScheduledExecutorService executorService,
int pollingIntervalMs,
int pollingSleepMs,
@NonNull PipelineCacheConfigurationProperties pipelineCacheConfigurationProperties,
ObjectMapper objectMapper,
@NonNull Front50Service front50,
@NonNull OrcaService orca,
@NonNull Registry registry,
@NonNull List triggerHandlers) {
this.objectMapper = objectMapper;
this.executorService = executorService;
this.pollingIntervalMs = pollingIntervalMs;
this.pollingSleepMs = pollingSleepMs;
this.pipelineCacheConfigurationProperties = pipelineCacheConfigurationProperties;
this.front50 = front50;
this.orca = orca;
this.registry = registry;
this.running = false;
this.pipelines = null;
this.triggersByType = null;
/**
* triggerHandlers are the main consumers of the pipelines here. Only query front50 for
* (enabled) pipelines with (enabled) triggers that these handlers support, as well as cron
* triggers, which PipelineConfigsPollingJob and MissedPipelineTriggerCompensationJob also
* consume. This handles the majority of cases, but there's one missing -- manual execution of
* pipelines. There's additional logic in ManualEventHandler to retrieve the pipeline in
* question for front50 when it's not present here in the cache.
*/
Stream triggerTypesStream =
triggerHandlers.stream()
.map(TriggerEventHandler::supportedTriggerTypes)
.flatMap(List::stream);
// Sort to provide a predictable order which facilitates testing.
this.supportedTriggerTypes =
Stream.concat(triggerTypesStream, Stream.of(Trigger.Type.CRON.toString()))
.sorted()
.distinct()
.collect(Collectors.joining(","));
log.info("supportedTriggerTypes: {}", supportedTriggerTypes);
}
@PreDestroy
public void stop() {
running = false;
executorService.shutdown();
}
@PostConstruct
public void start() {
running = true;
executorService.scheduleWithFixedDelay(
new Runnable() {
@Override
public void run() {
pollPipelineConfigs();
}
},
0,
pollingIntervalMs,
TimeUnit.MILLISECONDS);
PolledMeter.using(registry)
.withName("front50.lastPoll")
.monitorValue(this, PipelineCache::getDurationSeconds);
}
/**
* Accessor for whether we're filtering front50 pipelines or not. Yes, this information is
* available via PipelineCacheConfigurationProperties, but it seems cleaner to provide it directly
* this way, so users of PipelineCache don't need to care how we're configured. `
*/
public boolean isFilterFront50Pipelines() {
return pipelineCacheConfigurationProperties.isFilterFront50Pipelines();
}
private Double getDurationSeconds() {
return (lastPollTimestamp == null)
? -1d
: (double) Duration.between(lastPollTimestamp, now()).getSeconds();
}
// VisibleForTesting
void pollPipelineConfigs() {
if (!isRunning()) {
return;
}
try {
log.debug("Getting pipelines from Front50...");
long start = System.currentTimeMillis();
pipelines = fetchHydratedPipelines();
// refresh the triggers view every time we fetch the latest pipelines
triggersByType = extractEnabledTriggersFrom(pipelines);
lastPollTimestamp = now();
registry.counter("front50.requests").increment();
log.debug(
"Fetched {} pipeline configs in {}ms",
pipelines.size(),
System.currentTimeMillis() - start);
} catch (Exception e) {
log.error("Error fetching pipelines from Front50", e);
registry.counter("front50.errors").increment();
}
}
private Map hydrate(Map rawPipeline) {
Predicate
© 2015 - 2024 Weber Informatics LLC | Privacy Policy