How to export metrics to Prometheus via PushGateway using Spring Boot 2.0 - spring-boot

I upgraded Spring Boot version from 1.5.x to 2.0.1, but struggled with an issue of new Metrics.
To use micrometer In Spring boot 2.0+, I must remove the <dependency/> of micrometer-spring-legacy. Unfortunately, all of the config of management.metrics.export.prometheus.pushgateway disappeared. So how can I export metrics to pushgateway using spring boot 2.0?
Many thanks!

Unfortunately the Prometheus Pushgateway auto-configuration hasn't made it into Spring-Boot 2. Unsure if a PR which incorporates the micromter-spring-legacy setup would be accepted.
In the meantime you could try to setup your own #Configuration class which includes everything starting here.
Here's a quickly stitched together solution:
#Configuration
#EnableConfigurationProperties(PushgatewayProperties.class)
public class PushgatewayConfiguration {
#ConfigurationProperties(prefix = "management.metrics.export.prometheus.pushgateway")
public static class PushgatewayProperties {
/**
* Enable publishing via a Prometheus Pushgateway.
*/
private Boolean enabled = false;
/**
* Required host:port or ip:port of the Pushgateway.
*/
private String baseUrl = "localhost:9091";
/**
* Required identifier for this application instance.
*/
private String job;
/**
* Frequency with which to push metrics to Pushgateway.
*/
private Duration pushRate = Duration.ofMinutes(1);
/**
* Push metrics right before shut-down. Mostly useful for batch jobs.
*/
private boolean pushOnShutdown = true;
/**
* Delete metrics from Pushgateway when application is shut-down
*/
private boolean deleteOnShutdown = true;
/**
* Used to group metrics in pushgateway. A common example is setting
*/
private Map<String, String> groupingKeys = new HashMap<>();
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public Duration getPushRate() {
return pushRate;
}
public void setPushRate(Duration pushRate) {
this.pushRate = pushRate;
}
public boolean isPushOnShutdown() {
return pushOnShutdown;
}
public void setPushOnShutdown(boolean pushOnShutdown) {
this.pushOnShutdown = pushOnShutdown;
}
public boolean isDeleteOnShutdown() {
return deleteOnShutdown;
}
public void setDeleteOnShutdown(boolean deleteOnShutdown) {
this.deleteOnShutdown = deleteOnShutdown;
}
public Map<String, String> getGroupingKeys() {
return groupingKeys;
}
public void setGroupingKeys(Map<String, String> groupingKeys) {
this.groupingKeys = groupingKeys;
}
}
static class PrometheusPushGatewayEnabledCondition extends AllNestedConditions {
public PrometheusPushGatewayEnabledCondition() {
super(ConfigurationPhase.PARSE_CONFIGURATION);
}
#ConditionalOnProperty(value = "management.metrics.export.prometheus.enabled", matchIfMissing = true)
static class PrometheusMeterRegistryEnabled {
//
}
#ConditionalOnProperty("management.metrics.export.prometheus.pushgateway.enabled")
static class PushGatewayEnabled {
//
}
}
/**
* Configuration for
* <a href="https://github.com/prometheus/pushgateway">Prometheus
* Pushgateway</a>.
*
* #author David J. M. Karlsen
*/
#Configuration
#ConditionalOnClass(PushGateway.class)
#Conditional(PrometheusPushGatewayEnabledCondition.class)
#Incubating(since = "1.0.0")
public class PrometheusPushGatewayConfiguration {
private final Logger logger = LoggerFactory.getLogger(PrometheusPushGatewayConfiguration.class);
private final CollectorRegistry collectorRegistry;
private final PushgatewayProperties pushgatewayProperties;
private final PushGateway pushGateway;
private final Environment environment;
private final ScheduledExecutorService executorService;
PrometheusPushGatewayConfiguration(CollectorRegistry collectorRegistry,
PushgatewayProperties pushgatewayProperties, Environment environment) {
this.collectorRegistry = collectorRegistry;
this.pushgatewayProperties = pushgatewayProperties;
this.pushGateway = new PushGateway(pushgatewayProperties.getBaseUrl());
this.environment = environment;
this.executorService = Executors.newSingleThreadScheduledExecutor((r) -> {
final Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("micrometer-pushgateway");
return thread;
});
executorService.scheduleAtFixedRate(this::push, 0, pushgatewayProperties.getPushRate().toMillis(),
TimeUnit.MILLISECONDS);
}
void push() {
try {
pushGateway.pushAdd(collectorRegistry, job(), pushgatewayProperties.getGroupingKeys());
} catch (UnknownHostException e) {
logger.error("Unable to locate host '" + pushgatewayProperties.getBaseUrl()
+ "'. No longer attempting metrics publication to this host");
executorService.shutdown();
} catch (Throwable t) {
logger.error("Unable to push metrics to Prometheus Pushgateway", t);
}
}
#PreDestroy
void shutdown() {
executorService.shutdown();
if (pushgatewayProperties.isPushOnShutdown()) {
push();
}
if (pushgatewayProperties.isDeleteOnShutdown()) {
try {
pushGateway.delete(job(), pushgatewayProperties.getGroupingKeys());
} catch (Throwable t) {
logger.error("Unable to delete metrics from Prometheus Pushgateway", t);
}
}
}
private String job() {
String job = pushgatewayProperties.getJob();
if (job == null) {
job = environment.getProperty("spring.application.name");
}
if (job == null) {
// There's a history of Prometheus spring integration defaulting the job name to
// "spring" from when
// Prometheus integration didn't exist in Spring itself.
job = "spring";
}
return job;
}
}
}

Related

Error in adding a custom BackOffPolicy to spring-retry

I am facing error in adding a custom BackOffPolicy to spring-retry I followed the approach from Incremental linear backoff in Spring Retry .
My Retryable method is
#Autowired
private RetryOperationsInterceptor retryOperationsInterceptor;
#Override
#Retryable(
interceptor = "retryOperationsInterceptor",
maxAttemptsExpression = "#{${customer.maxRetries:8}}",
value=Exception.class
)
public customer getcustomer(String abc) {
LOGGER.info("Trying to fetch customer details.");
}
#Recover
public customer recover(RuntimeException e, String abc){
LOGGER.info("Recovering - returning safe value");
return null;
}
My configuration file is
#EnableRetry
public class RetryConfigurations {
#Autowired
private LinearBackoffPolicy linearBackoffPolicy;
#Bean(name = "retryOperationsInterceptor")
RetryOperationsInterceptor retryOperationsInterceptor() {
return RetryInterceptorBuilder.stateless().backOffPolicy(linearBackoffPolicy).build();
}
I defined a simple linearBackOffPolicy as
package com.ncr.digitalbanking.prism.connector.configuration;
import org.springframework.retry.backoff.BackOffInterruptedException;
import org.springframework.retry.backoff.Sleeper;
import org.springframework.retry.backoff.SleepingBackOffPolicy;
import org.springframework.retry.backoff.StatelessBackOffPolicy;
import org.springframework.retry.backoff.ThreadWaitSleeper;
import org.springframework.stereotype.Component;
#Component
public class LinearBackoffPolicy extends StatelessBackOffPolicy implements SleepingBackOffPolicy<LinearBackoffPolicy>{
private static final long DEFAULT_BACK_OFF_PERIOD = 1000L;
private volatile long backOffPeriod = DEFAULT_BACK_OFF_PERIOD;
private volatile long backOffDelta = DEFAULT_BACK_OFF_PERIOD;
private Sleeper sleeper = new ThreadWaitSleeper();
public LinearBackoffPolicy withSleeper(Sleeper sleeper) {
LinearBackoffPolicy linearBackoffPolicy = new LinearBackoffPolicy();
linearBackoffPolicy.setBackOffPeriod(backOffPeriod);
linearBackoffPolicy.setSleeper(sleeper);
return linearBackoffPolicy;
}
public long getBackOffPeriod() {
return backOffPeriod;
}
public void setBackOffPeriod(long backOffPeriod) {
this.backOffPeriod = (backOffPeriod > 0? backOffPeriod : 500);
}
public void setSleeper(Sleeper sleeper) {
this.sleeper = sleeper;
}
protected void doBackOff() throws BackOffInterruptedException {
try {
sleeper.sleep(backOffPeriod);
backOffPeriod = backOffPeriod + backOffDelta;
}
catch (InterruptedException e) {
throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
}
}
public String toString() {
return "LinearBackoffPolicy [backOffPeriod=" + backOffPeriod + "]";
}
}
Problems
Unable to use linear backoff policy in the rest call.
Unable to invoke Recover method after all retry attempt fails.
Note
My project works fine with this configuration
#Retryable(
interceptor = "retryOperationsInterceptor",
maxAttemptsExpression = "#{${prism.cas.maxRetries:8}}",
value=Exception.class
backoff = #Backoff(
delayExpression = "#{${prism.cas.delayExpression:10000}}",
multiplier = 2,
maxDelay = 12000
)
)
Did I mess up something?
#Recover doesn't work with a manually configured interceptor; it only works when the framework builds the interceptor.
You need to add .recoverer(...) to your interceptor builder.
Also
/**
* Retry interceptor bean name to be applied for retryable method. Is mutually
* exclusive with other attributes.
* #return the retry interceptor bean name
*/
String interceptor() default "";
your maxAttemptsExpression is ignored; the max attempts also has to go on the builder.

camelContext attribute discriminator for tenant resolver , using jpa multitenant and camel routeId

i ask you how can use camelContext to get the name of route fired by an event, more in details, how can I use any kind of discriminator attribute x in camelContext for predicate decision (if x =1 then .. else ..)
For example:
I have this kind of route:
//this route use the forst database
from("direct:csvprocessor1")
.routeId("tenant1")
.from("file:src/main/resources/data/1?move=OUT&moveFailed=REFUSED")
.unmarshal(csv)
.to("bean:myCsvHandler?method=doHandleCsvData")
.setBody(constant("OK VB"))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
.setHeader(Exchange.CONTENT_TYPE, constant("text/html"));
and this other route:
//this route use tenant2, the second database
from("direct:csvprocessor1")
.routeId("tenant2")
.from("file:src/main/resources/data/2?move=OUT&moveFailed=REFUSED")
.unmarshal(csv)
.to("bean:myCsvHandler?method=doHandleCsvData")
.setBody(constant("OK 2"))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
.setHeader(Exchange.CONTENT_TYPE, constant("text/html"));
when i pick up file in 1 folder the first route named "tenant1" starts, the same happen when pick up file in 2, the second route tenant2 starts.It reads csv content and the content must be write using jpa on the right tenantX (database)
I have to retrieve routeid name in another class but this class instanced before the camel Context start so i can't inject context (because this class "BatchCurrentTenantIdentifierResolverImpl " belong to Spring database initializator). I try to add method "of" to set camelContext but i get tenant1 only, also when route 2 starts, so can't switch from tenant to another tenant (tenant is database, i have two database):
#Component
public class BatchTenantContext {
private static final Logger log = LoggerFactory.getLogger(BatchTenantContext.class);
// don't Inject, use method Of because injecton was null
CamelContext cctx;
public BatchTenantContext(){getInstance();}
private final static BatchTenantContext instance = new BatchTenantContext();
public static BatchTenantContext getInstance(){
return instance;
}
public synchronized String get() {
if (cctx != null){
Route val = cctx.getRoute("tenant1");
if (val == null){
val = cctx.getRoute("tenant2");
if (val == null){
return "";
}
else {
return "tenant_2";
}
}
else return "tenant_1";
}
return "";
}
public synchronized void of(CamelContext ctx){
cctx = ctx;
}
public CamelContext getCamelContext()
{
return cctx;
}
}
//multitenant approach, switch from one database to another
//based on BatchTenantContext resolution..
public class BatchCurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {
static final Logger log = LoggerFactory.getLogger(BatchCurrentTenantIdentifierResolverImpl.class);
#Override
public String resolveCurrentTenantIdentifier() {
String val = BatchTenantContext.getInstance().get();
log.info("*** get tenant " + val);
return val;
}
#Override
public boolean validateExistingCurrentSessions() {
return true;
}
}
So, how to know how route fire? Note thaht the class above is singleton..I'm in a right way?
I use jpa whitin hibernate provider, configured using rhe multitenant configuration like this post: http://tech.asimio.net/2017/01/17/Multitenant-applications-using-Spring-Boot-JPA-Hibernate-and-Postgres.html
The application work in spring-boot Runtime environment or with Tomcat app server.
Any ideas about all?
Thanks so much!
roby
I add this code:
#Configuration
#EnableConfigurationProperties({ MultiTenantAfSissProperties.class, JpaProperties.class })
#ImportResource(locations = { "classpath:applicationContent.xml" })
#EnableTransactionManagement
public class MultiTenantJpaConfiguration {
static final Logger logger = LoggerFactory.getLogger(MultiTenantJpaConfiguration.class);
#Inject
private JpaProperties jpaProperties;
#Inject
MultiTenantAFSISSProperties multiTenantAFSISSProperties; //lista dei datasources collegati ai tenant
#Bean
public Map<String, DataSource> dataSourceRetrieval(){
Map<String, DataSource> result = new HashMap<>();
for (DataSourceProperties dsProperties : this.multiTenantAFSISSProperties.getDataSources()) {
DataSourceBuilder factory = DataSourceBuilder
.create()
.url(dsProperties.getUrl())
.username(dsProperties.getUsername())
.password(dsProperties.getPassword())
.driverClassName(dsProperties.getDriverClassName());
result.put(dsProperties.getTenantId(), factory.build());
}
return result;
}
/**
*
* #return
*/
#Bean
public MultiTenantConnectionProvider multiTenantConnectionProvider(){
return new AfsissMultiTenantConnectionProviderImpl();
}
/**
*
* #return
*/
#Bean
public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
return new BatchCurrentTenantIdentifierResolverImpl();
}
/**
*
* #param multiTenantConnectionProvider
* #param currentTenantIdentifierResolver
* #return
*/
#Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(MultiTenantConnectionProvider multiTenantConnectionProvider,
CurrentTenantIdentifierResolver currentTenantIdentifierResolver) {
Map<String, Object> hibernateProps = new LinkedHashMap<>();
hibernateProps.putAll(this.jpaProperties.getProperties());
Map<String,String> all = this.jpaProperties.getProperties();
for ( Map.Entry<String, String> prop : all.entrySet()){
System.out.println(" " + prop.getKey() + " = " + prop.getValue());
}
hibernateProps.put(Environment.MULTI_TENANT, MultiTenancyStrategy.DATABASE);
hibernateProps.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider);
hibernateProps.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver);
// No dataSource is set to resulting entityManagerFactoryBean
LocalContainerEntityManagerFactoryBean result = new LocalContainerEntityManagerFactoryBean();
result.setPackagesToScan(new String[] { AfFileEntity.class.getPackage().getName() });
result.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
result.setJpaPropertyMap(hibernateProps);
return result;
}
/**
* crea la factory per ricavare l'entity manager
* #param entityManagerFactoryBean
* #return
*/
#Bean
public EntityManagerFactory entityManagerFactory(LocalContainerEntityManagerFactoryBean entityManagerFactoryBean) {
return entityManagerFactoryBean.getObject();
}
/**
* get transaction manager
* #param entityManagerFactory
* #return
*/
#Bean
public PlatformTransactionManager txManager(EntityManagerFactory entityManagerFactory) {
SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class);
HibernateTransactionManager result = new HibernateTransactionManager();
result.setAutodetectDataSource(false);
result.setSessionFactory(sessionFactory);
return result;
}
}
In applicationContent.xml:
<jpa:repositories base-package="com.xxx.dao" transaction-manager-ref="txManager" />
<tx:annotation-driven transaction-manager="txManager" proxy-target-class="true" />
The class BatchCurrentTenantIdentifierResolverImpl is called in currentTenantIdentifierResolver() method above by spring transaction manager every time i use entity manager and transaction manager in csvHanlder :
#Component
#Transactional(propagation = Propagation.REQUIRED)
public class MyCsvHandler {
#Inject
AFMOVCrudRepository _entitymanagerMov; //it extends JpaRepository
#Inject
AFVINCCrudRepository _entityManagerVINC;//it extends JpaRepository
#Inject
AFFileCrudRepository _entityManagerAfFile;//it extends JpaRepository
static final Logger logger = LoggerFactory.getLogger(MyCsvHandler.class);
//save csv data on the right table on the right tenant
public void doHandleCsvData(List<List<String>> csvData) throws FileNotEvaluableException
{
//System.out.println("stampo..");
boolean status = true;
if (csvData.size() > 0){
AfFileEntity afbean = new AfFileEntity();
afbean.setNomeFile("test");
afbean.setDataImport(new java.sql.Timestamp(System.currentTimeMillis()));
afbean.setTipoFile("M");
afbean.setAfStatoFlusso("I");
_entityManagerAfFile.save(afbean);
long pkfile = afbean.getId();
logger.info("pkfile: " + pkfile);
int i = 1;
logger.info("file size:" + csvData.size());
for (List<String> rows : csvData){
//for (int j = 0; i < rows.size(); j++){
if (rows.get(2).trim().equalsIgnoreCase(...)){
MovEntity mbean = new MovEntity();
setMovFields(mbean, rows);
mbean.setAfFileId(afbean);
logger.info(String.valueOf((i++)) + " " + mbean);
_entitymanagerMov.save(mbean);
}
else if (rows.get(2).trim().equalsIgnoreCase(..) || rows.get(2).trim().equalsIgnoreCase(..) ) {
VincEntity vincBean = new VincEntity();
setVincFields(vincBean, rows);
vincBean.setAfFileId(afbean);
logger.info(String.valueOf((i++)) + " " + vincBean);
_entityManagerVINC.save(vincBean);
}
else {
status = false;
break;
}
}
if (!status) throw new FileNotEvaluableException("error file format");
}
}
private void setVincFields(VincEntity vincBean, List<String> rows) {
vincBean.setXXX().. and others methods
}
private void setMovFields(MovEntity mbean, List<String> rows) {
mbean.setStxxx() and other .. methods
}
return new
Something like this in your routes
from("direct:csvprocessor1").routeId("tenant2").process((Exchange e)-> {
BatchCurrentTenantIdentifierResolverImpl.tenant.set("tenant_1");
})
.from("file:src/main/resources/data/2?move=OUT&moveFailed=REFUSED")
.unmarshal().csv()
.to("bean:myCsvHandler?method=doHandleCsvData")
.setBody(constant("OK 2"))
.setHeader(Exchange.HTTP_RESPONSE_CODE, constant(200))
.setHeader(Exchange.CONTENT_TYPE, constant("text/html"));
And in your BatchCurrentTenantIdentifierResolverImpl implement it aspublic
class BatchCurrentTenantIdentifierResolverImpl {
public static ThreadLocal<String> tenant = new ThreadLocal<String>();
static final Logger log = LoggerFactory.getLogger(BatchCurrentTenantIdentifierResolverImpl.class);
#Override
public String resolveCurrentTenantIdentifier() {
String val = tenant.get();
log.info("*** get tenant " + val);
return val;
}
#Override
public boolean validateExistingCurrentSessions() {
return true;
}
}

#RefreshScope annotated Bean registered through BeanDefinitionRegistryPostProcessor not getting refreshed on Cloud Config changes

I've a BeanDefinitionRegistryPostProcessor class that registers beans dynamically. Sometimes, the beans being registered have the Spring Cloud annotation #RefreshScope.
However, when the cloud configuration Environment is changed, such beans are not being refreshed. Upon debugging, the appropriate application events are triggered, however, the dynamic beans don't get reinstantiated. Need some help around this. Below is my code:
TestDynaProps:
public class TestDynaProps {
private String prop;
private String value;
public String getProp() {
return prop;
}
public void setProp(String prop) {
this.prop = prop;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TestDynaProps [prop=").append(prop).append(", value=").append(value).append("]");
return builder.toString();
}
}
TestDynaPropConsumer:
#RefreshScope
public class TestDynaPropConsumer {
private TestDynaProps props;
public void setProps(TestDynaProps props) {
this.props = props;
}
#PostConstruct
public void init() {
System.out.println("Init props : " + props);
}
public String getVal() {
return props.getValue();
}
}
BeanDefinitionRegistryPostProcessor:
public class PropertyBasedDynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private ConfigurableEnvironment environment;
private final Class<?> propertyConfigurationClass;
private final String propertyBeanNamePrefix;
private final String propertyKeysPropertyName;
private Class<?> propertyConsumerBean;
private String consumerBeanNamePrefix;
private List<String> dynaBeans;
public PropertyBasedDynamicBeanDefinitionRegistrar(Class<?> propertyConfigurationClass,
String propertyBeanNamePrefix, String propertyKeysPropertyName) {
this.propertyConfigurationClass = propertyConfigurationClass;
this.propertyBeanNamePrefix = propertyBeanNamePrefix;
this.propertyKeysPropertyName = propertyKeysPropertyName;
dynaBeans = new ArrayList<>();
}
public void setPropertyConsumerBean(Class<?> propertyConsumerBean, String consumerBeanNamePrefix) {
this.propertyConsumerBean = propertyConsumerBean;
this.consumerBeanNamePrefix = consumerBeanNamePrefix;
}
#Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {
}
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefRegistry) throws BeansException {
if (environment == null) {
throw new BeanCreationException("Environment must be set to initialize dyna bean");
}
String[] keys = getPropertyKeys();
Map<String, String> propertyKeyBeanNameMapping = new HashMap<>();
for (String k : keys) {
String trimmedKey = k.trim();
String propBeanName = getPropertyBeanName(trimmedKey);
registerPropertyBean(beanDefRegistry, trimmedKey, propBeanName);
propertyKeyBeanNameMapping.put(trimmedKey, propBeanName);
}
if (propertyConsumerBean != null) {
String beanPropertyFieldName = getConsumerBeanPropertyVariable();
for (Map.Entry<String, String> prop : propertyKeyBeanNameMapping.entrySet()) {
registerConsumerBean(beanDefRegistry, prop.getKey(), prop.getValue(), beanPropertyFieldName);
}
}
}
private void registerConsumerBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName, String beanPropertyFieldName) {
String consumerBeanName = getConsumerBeanName(trimmedKey);
AbstractBeanDefinition consumerDefinition = preparePropertyConsumerBeanDefinition(propBeanName, beanPropertyFieldName);
beanDefRegistry.registerBeanDefinition(consumerBeanName, consumerDefinition);
dynaBeans.add(consumerBeanName);
}
private void registerPropertyBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName) {
AbstractBeanDefinition propertyBeanDefinition = preparePropertyBeanDefinition(trimmedKey);
beanDefRegistry.registerBeanDefinition(propBeanName, propertyBeanDefinition);
dynaBeans.add(propBeanName);
}
private String getConsumerBeanPropertyVariable() throws IllegalArgumentException {
Field[] beanFields = propertyConsumerBean.getDeclaredFields();
for (Field bField : beanFields) {
if (bField.getType().equals(propertyConfigurationClass)) {
return bField.getName();
}
}
throw new BeanCreationException(String.format("Could not find property of type %s in bean class %s",
propertyConfigurationClass.getName(), propertyConsumerBean.getName()));
}
private AbstractBeanDefinition preparePropertyBeanDefinition(String trimmedKey) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(PropertiesConfigurationFactory.class);
bdb.addConstructorArgValue(propertyConfigurationClass);
bdb.addPropertyValue("propertySources", environment.getPropertySources());
bdb.addPropertyValue("conversionService", environment.getConversionService());
bdb.addPropertyValue("targetName", trimmedKey);
return bdb.getBeanDefinition();
}
private AbstractBeanDefinition preparePropertyConsumerBeanDefinition(String propBeanName, String beanPropertyFieldName) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(propertyConsumerBean);
bdb.addPropertyReference(beanPropertyFieldName, propBeanName);
return bdb.getBeanDefinition();
}
private String getPropertyBeanName(String trimmedKey) {
return propertyBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String getConsumerBeanName(String trimmedKey) {
return consumerBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String[] getPropertyKeys() {
String keysProp = environment.getProperty(propertyKeysPropertyName);
return keysProp.split(",");
}
The Config class:
#Configuration
public class DynaPropsConfig {
#Bean
public PropertyBasedDynamicBeanDefinitionRegistrar dynaRegistrar() {
PropertyBasedDynamicBeanDefinitionRegistrar registrar = new PropertyBasedDynamicBeanDefinitionRegistrar(TestDynaProps.class, "testDynaProp", "dyna.props");
registrar.setPropertyConsumerBean(TestDynaPropConsumer.class, "testDynaPropsConsumer");
return registrar;
}
}
Application.java
#SpringBootApplication
#EnableDiscoveryClient
#EnableScheduling
public class Application extends SpringBootServletInitializer {
private static Class<Application> applicationClass = Application.class;
public static void main(String[] args) {
SpringApplication sa = new SpringApplication(applicationClass);
sa.run(args);
}
}
And, my bootstrap.properties:
spring.cloud.consul.enabled=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=PROPERTIES
spring.cloud.consul.config.watch.delay=15000
spring.cloud.discovery.client.health-indicator.enabled=false
spring.cloud.discovery.client.composite-indicator.enabled=false
application.properties
dyna.props=d1,d2
d1.prop=d1prop
d1.value=d1value
d2.prop=d2prop
d2.value=d2value
Here are some guesses:
1) Perhaps the #RefreshScope metadata is not being passed to your metadata for the bean definition. Call setScope()?
2) The RefreshScope is actually implemented by https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java, which itself implements BeanDefinitionRegistryPostProcessor. Perhaps the ordering of these two post processors is issue.
Just guesses.
We finally resolved this by appending the #RefreshScope annotation on the proposed dynamic bean classes using ByteBuddy and then, adding them to Spring Context using Bean Definition Post Processor.
The Post Processor is added to spring.factories so that it loads before any other dynamic bean dependent beans.

spring boot mybatis with redis as cache

I'm using Spring Boot with Mybatis, and I noticed that everytime I fetch some data with the same query, it will connect to db and query without using cache. So I search the solution for Mybatis with Redis, but cannot find one. All the answer is for Spring and xml configuration file, and I think it's better to use annotation. So, how to configure Redis as Cache to Mybatis in Spring Boot.
here is one of my solution, it just doesn't work.
MybatisRedisCache:
public class MybatisRedisCache implements Cache {
private static Logger logger = Logger.getLogger(MybatisRedisCache.class);
private Jedis redisClient = createClient();
/** The ReadWriteLock. */
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private String id;
public MybatisRedisCache(final String id) {
if (id == null) {
throw new IllegalArgumentException("Cache instances require an ID");
}
logger.debug("MybatisRedisCache:id=" + id);
this.id = id;
}
#Override
public String getId() {
return this.id;
}
#Override
public int getSize() {
return Integer.valueOf(redisClient.dbSize().toString());
}
#Override
public void putObject(Object key, Object value) {
logger.debug("putObject:" + key + "=" + value);
redisClient.set(SerializeUtil.serialize(key.toString()), SerializeUtil.serialize(value));
}
#Override
public Object getObject(Object key) {
Object value = SerializeUtil.unserialize(redisClient.get(SerializeUtil.serialize(key.toString())));
logger.debug("getObject:" + key + "=" + value);
return value;
}
#Override
public Object removeObject(Object key) {
return redisClient.expire(SerializeUtil.serialize(key.toString()), 0);
}
#Override
public void clear() {
redisClient.flushDB();
}
#Override
public ReadWriteLock getReadWriteLock() {
return readWriteLock;
}
protected static Jedis createClient() {
try {
JedisPool pool = new JedisPool(new JedisPoolConfig(), "127.0.0.1");
return pool.getResource();
} catch (Exception e) {
e.printStackTrace();
}
throw new RuntimeException("connect failed");
}
}
And here is LoggingRedisCache
public class LoggingRedisCache extends LoggingCache {
public LoggingRedisCache(String id) {
super(new MybatisRedisCache(id));
}
}

Spring Scheduled Task running in clustered environment

I am writing an application that has a cron job that executes every 60 seconds. The application is configured to scale when required onto multiple instances. I only want to execute the task on 1 instance every 60 seconds (On any node). Out of the box I can not find a solution to this and I am surprised it has not been asked multiple times before. I am using Spring 4.1.6.
<task:scheduled-tasks>
<task:scheduled ref="beanName" method="execute" cron="0/60 * * * * *"/>
</task:scheduled-tasks>
There is a ShedLock project that serves exactly this purpose. You just annotate tasks which should be locked when executed
#Scheduled( ... )
#SchedulerLock(name = "scheduledTaskName")
public void scheduledTask() {
// do something
}
Configure Spring and a LockProvider
#Configuration
#EnableScheduling
#EnableSchedulerLock(defaultLockAtMostFor = "10m")
class MySpringConfiguration {
...
#Bean
public LockProvider lockProvider(DataSource dataSource) {
return new JdbcTemplateLockProvider(dataSource);
}
...
}
I think you have to use Quartz Clustering with JDBC-JobStore for this purpose
The is another simple and robust way to safe execute a job in a cluster. You can based on database and execute the task only if the node is the "leader" in the cluster.
Also when a node is failed or shutdown in the cluster another node became the leader.
All you have is to create a "leader election" mechanism and every time to check if your are the leader:
#Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
if (checkIfLeader()) {
final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
for (EmailTask emailTask : list) {
dispatchService.sendEmail(emailTask);
}
}
}
Follow those steps:
1.Define the object and table that holds one entry per node in the cluster:
#Entity(name = "SYS_NODE")
public class SystemNode {
/** The id. */
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** The name. */
#Column(name = "TIMESTAMP")
private String timestamp;
/** The ip. */
#Column(name = "IP")
private String ip;
/** The last ping. */
#Column(name = "LAST_PING")
private Date lastPing;
/** The last ping. */
#Column(name = "CREATED_AT")
private Date createdAt = new Date();
/** The last ping. */
#Column(name = "IS_LEADER")
private Boolean isLeader = Boolean.FALSE;
public Long getId() {
return id;
}
public void setId(final Long id) {
this.id = id;
}
public String getTimestamp() {
return timestamp;
}
public void setTimestamp(final String timestamp) {
this.timestamp = timestamp;
}
public String getIp() {
return ip;
}
public void setIp(final String ip) {
this.ip = ip;
}
public Date getLastPing() {
return lastPing;
}
public void setLastPing(final Date lastPing) {
this.lastPing = lastPing;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(final Date createdAt) {
this.createdAt = createdAt;
}
public Boolean getIsLeader() {
return isLeader;
}
public void setIsLeader(final Boolean isLeader) {
this.isLeader = isLeader;
}
#Override
public String toString() {
return "SystemNode{" +
"id=" + id +
", timestamp='" + timestamp + '\'' +
", ip='" + ip + '\'' +
", lastPing=" + lastPing +
", createdAt=" + createdAt +
", isLeader=" + isLeader +
'}';
}
}
2.Create the service that a) insert the node in database , b) check for leader
#Service
#Transactional
public class SystemNodeServiceImpl implements SystemNodeService, ApplicationListener {
/** The logger. */
private static final Logger LOGGER = Logger.getLogger(SystemNodeService.class);
/** The constant NO_ALIVE_NODES. */
private static final String NO_ALIVE_NODES = "Not alive nodes found in list {0}";
/** The ip. */
private String ip;
/** The system service. */
private SystemService systemService;
/** The system node repository. */
private SystemNodeRepository systemNodeRepository;
#Autowired
public void setSystemService(final SystemService systemService) {
this.systemService = systemService;
}
#Autowired
public void setSystemNodeRepository(final SystemNodeRepository systemNodeRepository) {
this.systemNodeRepository = systemNodeRepository;
}
#Override
public void pingNode() {
final SystemNode node = systemNodeRepository.findByIp(ip);
if (node == null) {
createNode();
} else {
updateNode(node);
}
}
#Override
public void checkLeaderShip() {
final List<SystemNode> allList = systemNodeRepository.findAll();
final List<SystemNode> aliveList = filterAliveNodes(allList);
SystemNode leader = findLeader(allList);
if (leader != null && aliveList.contains(leader)) {
setLeaderFlag(allList, Boolean.FALSE);
leader.setIsLeader(Boolean.TRUE);
systemNodeRepository.save(allList);
} else {
final SystemNode node = findMinNode(aliveList);
setLeaderFlag(allList, Boolean.FALSE);
node.setIsLeader(Boolean.TRUE);
systemNodeRepository.save(allList);
}
}
/**
* Returns the leaded
* #param list
* the list
* #return the leader
*/
private SystemNode findLeader(final List<SystemNode> list) {
for (SystemNode systemNode : list) {
if (systemNode.getIsLeader()) {
return systemNode;
}
}
return null;
}
#Override
public boolean isLeader() {
final SystemNode node = systemNodeRepository.findByIp(ip);
return node != null && node.getIsLeader();
}
#Override
public void onApplicationEvent(final ApplicationEvent applicationEvent) {
try {
ip = InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
throw new RuntimeException(e);
}
if (applicationEvent instanceof ContextRefreshedEvent) {
pingNode();
}
}
/**
* Creates the node
*/
private void createNode() {
final SystemNode node = new SystemNode();
node.setIp(ip);
node.setTimestamp(String.valueOf(System.currentTimeMillis()));
node.setCreatedAt(new Date());
node.setLastPing(new Date());
node.setIsLeader(CollectionUtils.isEmpty(systemNodeRepository.findAll()));
systemNodeRepository.save(node);
}
/**
* Updates the node
*/
private void updateNode(final SystemNode node) {
node.setLastPing(new Date());
systemNodeRepository.save(node);
}
/**
* Returns the alive nodes.
*
* #param list
* the list
* #return the alive nodes
*/
private List<SystemNode> filterAliveNodes(final List<SystemNode> list) {
int timeout = systemService.getSetting(SettingEnum.SYSTEM_CONFIGURATION_SYSTEM_NODE_ALIVE_TIMEOUT, Integer.class);
final List<SystemNode> finalList = new LinkedList<>();
for (SystemNode systemNode : list) {
if (!DateUtils.hasExpired(systemNode.getLastPing(), timeout)) {
finalList.add(systemNode);
}
}
if (CollectionUtils.isEmpty(finalList)) {
LOGGER.warn(MessageFormat.format(NO_ALIVE_NODES, list));
throw new RuntimeException(MessageFormat.format(NO_ALIVE_NODES, list));
}
return finalList;
}
/**
* Finds the min name node.
*
* #param list
* the list
* #return the min node
*/
private SystemNode findMinNode(final List<SystemNode> list) {
SystemNode min = list.get(0);
for (SystemNode systemNode : list) {
if (systemNode.getTimestamp().compareTo(min.getTimestamp()) < -1) {
min = systemNode;
}
}
return min;
}
/**
* Sets the leader flag.
*
* #param list
* the list
* #param value
* the value
*/
private void setLeaderFlag(final List<SystemNode> list, final Boolean value) {
for (SystemNode systemNode : list) {
systemNode.setIsLeader(value);
}
}
}
3.ping the database to send that your are alive
#Override
#Scheduled(cron = "0 0/5 * * * ?")
public void executeSystemNodePing() {
systemNodeService.pingNode();
}
#Override
#Scheduled(cron = "0 0/10 * * * ?")
public void executeLeaderResolution() {
systemNodeService.checkLeaderShip();
}
4.you are ready! Just check if you are the leader before execute the task:
#Override
#Scheduled(cron = "*/30 * * * * *")
public void executeFailedEmailTasks() {
if (checkIfLeader()) {
final List<EmailTask> list = emailTaskService.getFailedEmailTasks();
for (EmailTask emailTask : list) {
dispatchService.sendEmail(emailTask);
}
}
}
Batch and scheduled jobs are typically run on their own standalone servers, away from customer facing apps so it is not a common requirement to include a job in an application that is expected to run on a cluster. Additionally, jobs in clustered environments typically do not need to worry about other instances of the same job running in parallel so another reason why isolation of job instances is not a big requirement.
A simple solution would be to configure your jobs inside a Spring Profile. For example, if your current configuration is:
<beans>
<bean id="someBean" .../>
<task:scheduled-tasks>
<task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
</task:scheduled-tasks>
</beans>
change it to:
<beans>
<beans profile="scheduled">
<bean id="someBean" .../>
<task:scheduled-tasks>
<task:scheduled ref="someBean" method="execute" cron="0/60 * * * * *"/>
</task:scheduled-tasks>
</beans>
</beans>
Then, launch your application on just one machine with the scheduled profile activated (-Dspring.profiles.active=scheduled).
If the primary server becomes unavailable for some reason, just launch another server with the profile enabled and things will continue to work just fine.
Things change if you want automatic failover for the jobs as well. Then, you will need to keep the job running on all servers and check synchronization through a common resource such as a database table, a clustered cache, a JMX variable, etc.
I'm using a database table to do the locking. Only one task at a time can do a insert to the table. The other one will get a DuplicateKeyException.
The insert and delete logic is handeld by an aspect around the #Scheduled annotation.
I'm using Spring Boot 2.0
#Component
#Aspect
public class SchedulerLock {
private static final Logger LOGGER = LoggerFactory.getLogger(SchedulerLock.class);
#Autowired
private JdbcTemplate jdbcTemplate;
#Around("execution(#org.springframework.scheduling.annotation.Scheduled * *(..))")
public Object lockTask(ProceedingJoinPoint joinPoint) throws Throwable {
String jobSignature = joinPoint.getSignature().toString();
try {
jdbcTemplate.update("INSERT INTO scheduler_lock (signature, date) VALUES (?, ?)", new Object[] {jobSignature, new Date()});
Object proceed = joinPoint.proceed();
jdbcTemplate.update("DELETE FROM scheduler_lock WHERE lock_signature = ?", new Object[] {jobSignature});
return proceed;
}catch (DuplicateKeyException e) {
LOGGER.warn("Job is currently locked: "+jobSignature);
return null;
}
}
}
#Component
public class EveryTenSecondJob {
#Scheduled(cron = "0/10 * * * * *")
public void taskExecution() {
System.out.println("Hello World");
}
}
CREATE TABLE scheduler_lock(
signature varchar(255) NOT NULL,
date datetime DEFAULT NULL,
PRIMARY KEY(signature)
);
dlock is designed to run tasks only once by using database indexes and constraints. You can simply do something like below.
#Scheduled(cron = "30 30 3 * * *")
#TryLock(name = "executeMyTask", owner = SERVER_NAME, lockFor = THREE_MINUTES)
public void execute() {
}
See the article about using it.
You can use Zookeeper here to elect master instance and master instance will only run the scheduled job.
One implementation here is with Aspect and Apache Curator
#SpringBootApplication
#EnableScheduling
public class Application {
private static final int PORT = 2181;
#Bean
public CuratorFramework curatorFramework() {
CuratorFramework client = CuratorFrameworkFactory.newClient("localhost:" + PORT, new ExponentialBackoffRetry(1000, 3));
client.start();
return client;
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Aspect class
#Aspect
#Component
public class LeaderAspect implements LeaderLatchListener{
private static final Logger log = LoggerFactory.getLogger(LeaderAspect.class);
private static final String ELECTION_ROOT = "/election";
private volatile boolean isLeader = false;
#Autowired
public LeaderAspect(CuratorFramework client) throws Exception {
LeaderLatch ll = new LeaderLatch(client , ELECTION_ROOT);
ll.addListener(this);
ll.start();
}
#Override
public void isLeader() {
isLeader = true;
log.info("Leadership granted.");
}
#Override
public void notLeader() {
isLeader = false;
log.info("Leadership revoked.");
}
#Around("#annotation(com.example.apache.curator.annotation.LeaderOnly)")
public void onlyExecuteForLeader(ProceedingJoinPoint joinPoint) {
if (!isLeader) {
log.debug("I'm not leader, skip leader-only tasks.");
return;
}
try {
log.debug("I'm leader, execute leader-only tasks.");
joinPoint.proceed();
} catch (Throwable ex) {
log.error(ex.getMessage());
}
}
}
LeaderOnlyAnnotation
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface LeaderOnly {
}
Scheduled Task
#Component
public class HelloWorld {
private static final Logger log = LoggerFactory.getLogger(HelloWorld.class);
#LeaderOnly
#Scheduled(fixedRate = 1000L)
public void sayHello() {
log.info("Hello, world!");
}
}
I am using a different approach without need to setup a database for managing the lock between the nodes.
The component is called FencedLock and is provided by Hazelcast.
We're using it to prevent another node to make some operation (not necessarily linked to schedule) but it could also be used for sharing a locks between nodes for a schedule.
For doing this, we just set up two functions helper that can create different lock names:
#Scheduled(cron = "${cron.expression}")
public void executeMyScheduler(){
// This can also be a member of the class.
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Lock lock = hazelcastInstance.getCPSubsystem().getLock("mySchedulerName");
lock.lock();
try {
// do your schedule tasks here
} finally {
// don't forget to release lock whatever happens: end of task or any exceptions.
lock.unlock();
}
}
Alternatively you can also release automatically the lock after a delay: let say your cron job is running every hour, you can setup an automatic release after e.g. 50 minutes like this:
#Scheduled(cron = "${cron.expression}")
public void executeMyScheduler(){
// This can also be a member of the class.
HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance();
Lock lock = hazelcastInstance.getCPSubsystem().getLock("mySchedulerName");
if ( lock.tryLock ( 50, TimeUnit.MINUTES ) ) {
try {
// do your schedule tasks here
} finally {
// don't forget to release lock whatever happens: end of task or any exceptions.
lock.unlock();
}
} else {
// warning: lock has been released by timeout!
}
}
Note that this Hazelcast component works very good in a cloud based environment (e.g. k8s clusters) and without need to pay for an extra database.
Here is what you need to configure:
// We need to specify the name otherwise it can conflict with internal Hazelcast beans
#Bean("hazelcastInstance")
public HazelcastInstance hazelcastInstance() {
Config config = new Config();
config.setClusterName(hazelcastProperties.getGroup().getName());
NetworkConfig networkConfig = config.getNetworkConfig();
networkConfig.setPortAutoIncrement(false);
networkConfig.getJoin().getKubernetesConfig().setEnabled(hazelcastProperties.isNetworkEnabled())
.setProperty("service-dns", hazelcastProperties.getServiceDNS())
.setProperty("service-port", hazelcastProperties.getServicePort().toString());
config.setProperty("hazelcast.metrics.enabled", "false");
networkConfig.getJoin().getMulticastConfig().setEnabled(false);
return Hazelcast.newHazelcastInstance(config);
}
The HazelcastProperties being the ConfigurationProperties object mapped with the properties.
For local testing you can just disable the network config by using the properties in your local profile:
hazelcast:
network-enabled: false
service-port: 5701
group:
name: your-hazelcast-group-name
You could use an embeddable scheduler like db-scheduler to accomplish this. It has persistent executions and uses a simple optimistic locking mechanism to guarantee execution by a single node.
Example code for how the use-case can be achieved:
RecurringTask<Void> recurring1 = Tasks.recurring("my-task-name", FixedDelay.of(Duration.ofSeconds(60)))
.execute((taskInstance, executionContext) -> {
System.out.println("Executing " + taskInstance.getTaskAndInstance());
});
final Scheduler scheduler = Scheduler
.create(dataSource)
.startTasks(recurring1)
.build();
scheduler.start();
I am using an free HTTP service called kJob-Manager. https://kjob-manager.ciesielski-systems.de/
Advantage is that you dont create a new table in your database and also dont need any database connection because it is just a HTTP request.
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import org.apache.tomcat.util.json.JSONParser;
import org.apache.tomcat.util.json.ParseException;
import org.junit.jupiter.api.Test;
public class KJobManagerTest {
#Test
public void example() throws IOException, ParseException {
String data = "{\"token\":\"<API-Token>\"}";
URL url = new URL("https://kjob-manager.ciesielski-systems.de/api/ticket/<JOB-ID>");
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.getOutputStream().write(data.getBytes(StandardCharsets.UTF_8));
JSONParser jsonParser = new JSONParser(connection.getInputStream());
LinkedHashMap<String, LinkedHashMap<String, Object>> result = (LinkedHashMap<String, LinkedHashMap<String, Object>>) jsonParser.parse();
if ((boolean) result.get("ticket").get("open")) {
System.out.println("This replica could run the cronjob!");
} else {
System.out.println("This replica has nothing to do!");
}
}
}
Spring context is not clustered so manage the task in distributed application is a little bit difficult and you need to use systems supporting jgroup to synchronis the state and let your task take the priority to execute the action. Or you could use ejb context to manage clustered ha singleton service like jboss ha environment
https://developers.redhat.com/quickstarts/eap/cluster-ha-singleton/?referrer=jbd
Or you could use clustered cache and access lock resource between the service and first service take the lock will beform the action or implement you own jgroup to communicat your service and perform the action one one node

Resources