io.gravitee.gateway.services.sync.cache.ApiKeysCacheService Maven / Gradle / Ivy
/*
* Copyright © 2015 The Gravitee team (http://gravitee.io)
*
* 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 io.gravitee.gateway.services.sync.cache;
import io.gravitee.common.event.Event;
import io.gravitee.common.event.EventListener;
import io.gravitee.common.event.EventManager;
import io.gravitee.common.http.MediaType;
import io.gravitee.common.service.AbstractService;
import io.gravitee.gateway.api.service.ApiKeyService;
import io.gravitee.gateway.reactor.Reactable;
import io.gravitee.gateway.reactor.ReactableApi;
import io.gravitee.gateway.reactor.ReactorEvent;
import io.gravitee.gateway.services.sync.cache.handler.ApiKeysServiceHandler;
import io.gravitee.gateway.services.sync.cache.task.FullApiKeyRefresher;
import io.gravitee.gateway.services.sync.cache.task.IncrementalApiKeyRefresher;
import io.gravitee.gateway.services.sync.cache.task.Result;
import io.gravitee.node.api.cluster.ClusterManager;
import io.gravitee.repository.management.api.ApiKeyRepository;
import io.gravitee.repository.management.api.SubscriptionRepository;
import io.vertx.ext.web.Router;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* @author David BRASSELY (david.brassely at graviteesource.com)
* @author GraviteeSource Team
*/
public class ApiKeysCacheService extends AbstractService implements EventListener {
private static final Logger LOGGER = LoggerFactory.getLogger(ApiKeysCacheService.class);
private static final String PATH = "/apikeys";
private final ThreadPoolTaskScheduler scheduler;
private final Map> plansPerApi = new ConcurrentHashMap<>();
private long startTime;
@Value("${services.sync.bulk_items:100}")
private int bulkItems;
@Value("${services.sync.distributed:false}")
private boolean distributed;
@Autowired
private EventManager eventManager;
@Autowired
private ApiKeyRepository apiKeyRepository;
@Autowired
private SubscriptionRepository subscriptionRepository;
@Autowired
private ApiKeyService apiKeyService;
@Autowired
@Qualifier("syncExecutor")
private ThreadPoolExecutor executorService;
@Autowired
@Qualifier("managementRouter")
private Router router;
@Autowired
private ClusterManager clusterManager;
private ScheduledFuture> scheduledFuture;
public ApiKeysCacheService() {
scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("gio.sync-apikeys-");
// Ensure every execution is done before running next execution
scheduler.setPoolSize(1);
scheduler.initialize();
}
@Override
protected void doStart() throws Exception {
super.doStart();
this.startTime = System.currentTimeMillis();
// Set API-keys handler
LOGGER.info("Associate a new HTTP handler on {}", PATH);
ApiKeysServiceHandler apiKeysHandler = new ApiKeysServiceHandler(executorService);
applicationContext.getAutowireCapableBeanFactory().autowireBean(apiKeysHandler);
router.get(PATH).produces(MediaType.APPLICATION_JSON).handler(apiKeysHandler);
}
public void startScheduler(int delay, TimeUnit unit) {
scheduledFuture = scheduler.scheduleAtFixedRate(new ApiKeysTask(), Duration.ofMillis(unit.toMillis(delay)));
eventManager.subscribeForEvents(this, ReactorEvent.class);
}
@Override
protected void doStop() throws Exception {
super.doStop();
if (scheduledFuture != null) {
scheduledFuture.cancel(false);
}
if (scheduler != null) {
scheduler.shutdown();
}
}
@Override
protected String name() {
return "API keys cache";
}
@Override
public void onEvent(Event event) {
switch (event.type()) {
case DEPLOY:
register((ReactableApi>) event.content());
break;
case UNDEPLOY:
unregister((ReactableApi>) event.content());
break;
case UPDATE:
unregister((ReactableApi>) event.content());
register((ReactableApi>) event.content());
break;
default:
// Nothing to do with unknown event type
break;
}
}
public void register(ReactableApi> api) {
register(Collections.singletonList(api));
}
public void register(List> apis) {
final Map> apisById = apis.stream().collect(Collectors.toMap(ReactableApi::getId, api -> api));
// Filters plans to update api_keys only for them
final Map> plansByApi = apis
.stream()
.filter(ReactableApi::isEnabled)
.map(api -> new AbstractMap.SimpleEntry<>(api.getId(), api.getApiKeyPlans()))
// Remove if no plan.
.filter(e -> !e.getValue().isEmpty())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
if (!plansByApi.isEmpty()) {
final Set planIds = plansByApi.values().stream().flatMap(Collection::stream).collect(Collectors.toSet());
// If the node is not a master, we assume that the full refresh has been handle by another node
if (clusterManager.isMasterNode() || (!clusterManager.isMasterNode() && !distributed)) {
final FullApiKeyRefresher refresher = new FullApiKeyRefresher(planIds);
refresher.setApiKeyRepository(apiKeyRepository);
refresher.setSubscriptionRepository(subscriptionRepository);
refresher.setApiKeyService(apiKeyService);
CompletableFuture
.supplyAsync(refresher::call, executorService)
.whenComplete((result, throwable) -> {
if (throwable != null) {
// An error occurs, we must try to full refresh again
register(apis);
} else {
// Once we are sure that the initial full refresh is a success, we cn move the plans to an incremental refresh
if (result.succeeded()) {
// Attach the plans to the global list
plansPerApi.putAll(plansByApi);
} else {
LOGGER.error(
"An error occurs while doing a full api-keys refresh for APIs [{}]",
apisById.keySet(),
result.cause()
);
// If not, try to fully refresh again
register(apis);
}
}
});
} else {
// Keep track of all the plans to ensure that, once the node is becoming a master node, we are able
// to run incremental refresh for all the plans
plansPerApi.putAll(plansByApi);
}
}
}
private void unregister(ReactableApi> api) {
plansPerApi.remove(api.getId());
}
class ApiKeysTask extends TimerTask {
private long lastRefreshAt = startTime;
@Override
public void run() {
if (clusterManager.isMasterNode() || (!clusterManager.isMasterNode() && !distributed)) {
long nextLastRefreshAt = System.currentTimeMillis();
// Merge all plans and split them into buckets
final Set plans = plansPerApi.values().stream().flatMap(Set::stream).collect(Collectors.toSet());
final AtomicInteger counter = new AtomicInteger();
final Collection> chunks = plans
.stream()
.collect(Collectors.groupingBy(it -> counter.getAndIncrement() / bulkItems))
.values();
// Run refreshers
if (!chunks.isEmpty()) {
// Prepare tasks
final List>> callables = chunks
.stream()
.map(chunk -> {
IncrementalApiKeyRefresher refresher = new IncrementalApiKeyRefresher(lastRefreshAt, nextLastRefreshAt, chunk);
refresher.setApiKeyRepository(apiKeyRepository);
refresher.setSubscriptionRepository(subscriptionRepository);
refresher.setApiKeyService(apiKeyService);
return refresher;
})
.collect(Collectors.toList());
// And run...
try {
List>> futures = executorService.invokeAll(callables);
boolean failure = futures
.stream()
.anyMatch(resultFuture -> {
try {
return resultFuture.get().failed();
} catch (Exception e) {
LOGGER.error("Unexpected error while running the api-keys refresher", e);
}
return false;
});
// If there is no failure, move to the next period of time
if (!failure) {
lastRefreshAt = nextLastRefreshAt;
}
} catch (InterruptedException e) {
LOGGER.error("Unexpected error while running the api-keys refresher");
Thread.currentThread().interrupt();
}
} else {
lastRefreshAt = nextLastRefreshAt;
}
}
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy