Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support lazy, fail-fast and background initialization of asyncapi #297

Merged
merged 10 commits into from
Jul 28, 2023
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.stavshamir.springwolf.asyncapi;

import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties.LoadingMode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.task.TaskExecutor;
import org.springframework.stereotype.Service;

/**
* Spring ApplicationListener listening on {@link ApplicationReadyEvent}. If loadingMode equals {@link LoadingMode#BACKGROUND}
* this listener triggers background AsyncAPI creation.
*/
@Slf4j
@RequiredArgsConstructor
@Service
@ConditionalOnProperty(name = "springwolf.loading-mode", havingValue = "BACKGROUND")
public class AsyncApiApplicationListener implements ApplicationListener<ApplicationReadyEvent> {

private final TaskExecutor taskExecutor;
private final AsyncApiService asyncApiService;

@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
log.debug("triggering background asyncapi creation..");
taskExecutor.execute(this.asyncApiService::getAsyncAPI);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@

import java.util.Map;

/**
* Service to detect AsyncAPI channels in the current spring context.
*/
public interface ChannelsService {

Map<String, ChannelItem> getChannels();
/**
* Detects all available AsyncAPI ChannelItem in the spring context.
*
* @return Map of channel names mapping to detected ChannelItems
*/
Map<String, ChannelItem> findChannels();
}
Original file line number Diff line number Diff line change
@@ -1,54 +1,113 @@
package io.github.stavshamir.springwolf.asyncapi;

import com.asyncapi.v2._6_0.model.channel.ChannelItem;
import io.github.stavshamir.springwolf.asyncapi.types.AsyncAPI;
import io.github.stavshamir.springwolf.asyncapi.types.Components;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocketService;
import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties;
import io.github.stavshamir.springwolf.schemas.SchemasService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;

import static io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties.LoadingMode.FAIL_FAST;

@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultAsyncApiService implements AsyncApiService {
public class DefaultAsyncApiService implements AsyncApiService, InitializingBean {

/**
* Record holding the result of AsyncAPI creation.
*
* @param asyncAPI
* @param exception
*/
private record AsyncAPIResult(AsyncAPI asyncAPI, Throwable exception) {}

private final AsyncApiDocketService asyncApiDocketService;
private final ChannelsService channelsService;
private final SchemasService schemasService;
private final List<AsyncApiCustomizer> customizers;

private AsyncAPI asyncAPI;
private final SpringWolfConfigProperties configProperties;

@PostConstruct
void buildAsyncApi() {
log.debug("Building AsyncAPI document");
private volatile AsyncAPIResult asyncAPIResult = null;

AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket();
@Override
public void afterPropertiesSet() {
if (configProperties.getLoadingMode() == FAIL_FAST) {
getAsyncAPI();
}
}

Components components =
Components.builder().schemas(schemasService.getDefinitions()).build();
@Override
public AsyncAPI getAsyncAPI() {
if (asyncAPIResult == null) {
initAsyncAPI();
}

asyncAPI = AsyncAPI.builder()
.info(docket.getInfo())
.id(docket.getId())
.defaultContentType(docket.getDefaultContentType())
.servers(docket.getServers())
.channels(channelsService.getChannels())
.components(components)
.build();
if (asyncAPIResult.asyncAPI != null) {
return asyncAPIResult.asyncAPI;
} else {
throw new RuntimeException("Error occured during creation of AsyncAPI", asyncAPIResult.exception);
}
}

for (AsyncApiCustomizer customizer : customizers) {
customizer.customize(asyncAPI);
/**
* Does the 'heavy work' of detecting the AsyncAPI documents. Stores the resulting
* AsyncAPI document or alternativly a catched exception/error in the instance variable asyncAPIResult.
*
* @return
*/
protected synchronized void initAsyncAPI() {
if (this.asyncAPIResult != null) {
return; // Double Check Idiom
}

try {
log.debug("Building AsyncAPI document");

AsyncApiDocket docket = asyncApiDocketService.getAsyncApiDocket();

// ChannelsService must be invoked before accessing SchemasService,
// because during channel scanning, all detected schemas are registered with
// SchemasService.
Map<String, ChannelItem> channels = channelsService.findChannels();

Components components = Components.builder()
.schemas(schemasService.getDefinitions())
.build();

AsyncAPI asyncAPI = AsyncAPI.builder()
.info(docket.getInfo())
.id(docket.getId())
.defaultContentType(docket.getDefaultContentType())
.servers(docket.getServers())
.channels(channels)
.components(components)
.build();

for (AsyncApiCustomizer customizer : customizers) {
customizer.customize(asyncAPI);
}
this.asyncAPIResult = new AsyncAPIResult(asyncAPI, null);
} catch (Throwable t) {
this.asyncAPIResult = new AsyncAPIResult(null, t);
}
}

@Override
public AsyncAPI getAsyncAPI() {
return asyncAPI;
/**
* checks whether asyncApi has internally allready been initialized. For testing purposes.
*
* @return true if asyncApi has been created and initialized.
*/
public synchronized boolean isInitialized() {
return this.asyncAPIResult != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,31 @@
import com.asyncapi.v2._6_0.model.channel.ChannelItem;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelMerger;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.ChannelsScanner;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* Service to detect AsyncAPI channels in the current spring context.
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DefaultChannelsService implements ChannelsService {

private final List<? extends ChannelsScanner> channelsScanners;
private final Map<String, ChannelItem> channels = new HashMap<>();

@PostConstruct
void findChannels() {
/**
* Collects all AsyncAPI ChannelItems using the available {@link ChannelsScanner}
* beans.
* @return Map of channel names mapping to detected ChannelItems
*/
@Override
public Map<String, ChannelItem> findChannels() {
List<Map.Entry<String, ChannelItem>> foundChannelItems = new ArrayList<>();

for (ChannelsScanner scanner : channelsScanners) {
Expand All @@ -33,12 +38,6 @@ void findChannels() {
log.error("An error was encountered during channel scanning with {}: {}", scanner, e.getMessage());
}
}

this.channels.putAll(ChannelMerger.merge(foundChannelItems));
}

@Override
public Map<String, ChannelItem> getChannels() {
return channels;
return ChannelMerger.merge(foundChannelItems);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,28 @@
@Setter
public class SpringWolfConfigProperties {

public enum LoadingMode {
/**
* AsyncAPI detection on application startup. Exceptions interrupt the application start.
*/
FAIL_FAST,
/**
* AsyncAPI detection lazy on first request.
*/
LAZY,
/**
* AsyncAPI detection after application startup in background thread (via Spring TaskExecutor).
*/
BACKGROUND
}

private boolean enabled = true;

/**
* Loading mode for AsyncAPI detection.
*/
private LoadingMode loadingMode = LoadingMode.LAZY;

@Nullable
private ConfigDocket docket;

Expand All @@ -44,15 +64,15 @@ public static class ConfigDocket {
/**
* Identifier of the application the AsyncAPI document is defining.
*
* @see com.asyncapi.v2._0_0.model.AsyncAPI#id
* @see com.asyncapi.v2._6_0.model.AsyncAPI#id
*/
@Nullable
private String id;

/**
* A string representing the default content type to use when encoding/decoding a message's payload.
*
* @see com.asyncapi.v2._0_0.model.AsyncAPI#defaultContentType
* @see com.asyncapi.v2._6_0.model.AsyncAPI#getdefaultContentType
*/
@Nullable
private String defaultContentType;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public class SpringContextTest {
@Nested
@ContextConfiguration(
classes = {
SpringWolfConfigProperties.class,
CustomBeanAsyncApiDocketConfiguration.class, // user has defined an own AsyncApiDocket bean
DefaultAsyncApiDocketService.class,
DefaultAsyncApiService.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.github.stavshamir.springwolf.asyncapi.types.channel.operation.message.Message;
import io.github.stavshamir.springwolf.configuration.AsyncApiDocket;
import io.github.stavshamir.springwolf.configuration.DefaultAsyncApiDocketService;
import io.github.stavshamir.springwolf.configuration.properties.SpringWolfConfigProperties;
import io.github.stavshamir.springwolf.schemas.DefaultSchemasService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
Expand All @@ -31,6 +32,7 @@
@ExtendWith(SpringExtension.class)
@ContextConfiguration(
classes = {
SpringWolfConfigProperties.class,
DefaultAsyncApiDocketService.class,
DefaultAsyncApiService.class,
DefaultChannelsService.class,
Expand Down
Loading