Dhaval
Dhaval

Reputation: 15

Spring Boot application should start even if Cassandra is down

I want to make sure that my App gets started even if Cassandra is down. What is the best way to handle that?

I am using AbstractReactiveCassandraConfiguration and multiple keyspace implementation.

    @Bean
    @NonNull
    @Override
    public CqlSessionFactoryBean cassandraSession() {
        final CqlSessionFactoryBean cqlSessionFactoryBean = new CqlSessionFactoryBean();
        cqlSessionFactoryBean.setContactPoints(contactPoints);
        cqlSessionFactoryBean.setLocalDatacenter(localDataCenter);
        cqlSessionFactoryBean.setUsername(username);
        cqlSessionFactoryBean.setPassword(password);
        return cqlSessionFactoryBean;
    }

Keyspace is set at the individual keyspace config file.

Keyspace 1 and keyspace 2 with similar kinds of configurations.

@Configuration
@EnableReactiveCassandraRepositories(basePackages = "com.abc.repository.cassandra.xyz", reactiveCassandraTemplateRef = "keyspaceXYZTemplate")
public class XYZCassandraConfiguration extends CassandraConfig {

    @Override
    protected String getKeyspaceName() {
        return "KEYSPACE_NAME";
    }

    @Bean("keyspaceXYZTemplate")
    public ReactiveCassandraTemplate reactiveCassandraTemplate(@Qualifier("xyzBean") CqlSessionFactoryBean cqlSessionFactoryBean) {
        final ReactiveSession reactiveSession = new DefaultBridgedReactiveSession(cqlSessionFactoryBean.getObject());
        return new ReactiveCassandraTemplate(reactiveSession, cassandraConverter());
    }

    @NonNull
    @Override
    @Primary
    @Bean(name = "xyzBean")
    public CqlSessionFactoryBean cassandraSession() {
        final CqlSessionFactoryBean cqlSessionFactoryBean = super.cassandraSession();
        cqlSessionFactoryBean.setKeyspaceName(getKeyspaceName());
        return cqlSessionFactoryBean;
    }
}

Stack Trace: When the app starts and Cassandra is down.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean
Invocation of init method failed; nested exception is com.datastax.oss.driver.api.core.AllNodesFailedException: Could not reach any contact point, make sure you've provided valid addresses (showing first 1 nodes, use getAllErrors() for more): Node(endPoint=localhost:9042, hostId=null, hashCode=70c95ec9): [com.datastax.oss.driver.api.core.connection.ConnectionInitException: [s0|control|connecting...] Protocol initialization request, step 1 (OPTIONS): failed to send request (io.netty.channel.StacklessClosedChannelException)]
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1354) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1204) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:564) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:524) ~[spring-beans-5.3.8.jar:5.3.8]
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) ~[spring-beans-5.3.8.jar:5.3.8]

Upvotes: 0

Views: 662

Answers (2)

Igor Ryabchuk
Igor Ryabchuk

Reputation: 21

In case anyone finds their way to this question, like I did, with the same need - ie start Spring app if Cassandra DB is down - it is possible!

If your set up utilises Cassandra Auto Configuration, the solution is in this question: Spring Boot and Spring Data with Cassandra: Continue on failed database connection

This very clever approach by spx01 fakes the CqlSession and it works.

If your set up utilises AbstractReactiveCassandraConfiguration, like mine, then you need to:

  1. Catch the AllNodesFailedException in CqlSessionFactoryBean like so:
    @Override
    @Bean
    public CqlSessionFactoryBean cassandraSession() {
        CqlSessionFactoryBean cassandraSession = super.cassandraSession();

        cassandraSession.setUsername(env.getProperty("spring.data.cassandra.username", "monkey"));
        cassandraSession.setPassword(env.getProperty("spring.data.cassandra.password", "likesBananas"));
        try {
            cassandraSession.afterPropertiesSet(); //this is needed so that we can trigger the exception before Spring tries to wire in the bean
            cassandraSession.destroy() //to close tcp connections/sessions opened by previous line
        } catch(AllNodesFailedException e) {
            return new NoOpCqlSessionFactoryBean();
        }
        return cassandraSession;
    }
  1. Set up your NoOp bean

Here, we are utilising DatabaseNotConnectedFakeCqlSession from spx01's solution mentioned above but in amended form (see point 3).

public class NoOpCqlSessionFactoryBean extends CqlSessionFactoryBean {

    @Override
    public CqlSession getObject() {
        return new DatabaseNotConnectedFakeCqlSession();
    }

    @Override
    public void afterPropertiesSet() {

    }
}
  1. Set up a fake cql session
public class DatabaseNotConnectedFakeCqlSession implements CqlSession {


    @NonNull
    @Override
    public String getName() {
        return null;
    }

    @NonNull
    @Override
    public Metadata getMetadata() {
        return null;
    }

    @Override
    public boolean isSchemaMetadataEnabled() {
        return false;
    }

    @NonNull
    @Override
    public CompletionStage<Metadata> setSchemaMetadataEnabled(@Nullable Boolean newValue) {
        return CompletableFuture.completedStage(new Metadata() {
            @NonNull
            @Override
            public Map<UUID, Node> getNodes() {
                return null;
            }

            @NonNull
            @Override
            public Map<CqlIdentifier, KeyspaceMetadata> getKeyspaces() {
                return null;
            }

            @NonNull
            @Override
            public Optional<TokenMap> getTokenMap() {
                return Optional.empty();
            }
        });
    }

    @NonNull
    @Override
    public CompletionStage<Metadata> refreshSchemaAsync() {
        return CompletableFuture.completedStage(new Metadata() {
            @NonNull
            @Override
            public Map<UUID, Node> getNodes() {
                return null;
            }

            @NonNull
            @Override
            public Map<CqlIdentifier, KeyspaceMetadata> getKeyspaces() {
                return null;
            }

            @NonNull
            @Override
            public Optional<TokenMap> getTokenMap() {
                return Optional.empty();
            }
        });
    }

    @NonNull
    @Override
    public CompletionStage<Boolean> checkSchemaAgreementAsync() {
        return null;
    }

    @NonNull
    @Override
    public DriverContext getContext() {
        return new NoOpDriverContext();
    }

    @NonNull
    @Override
    public Optional<CqlIdentifier> getKeyspace() {
        return Optional.empty();
    }

    @NonNull
    @Override
    public Optional<Metrics> getMetrics() {
        return Optional.empty();
    }

    @Nullable
    @Override
    public <RequestT extends Request, ResultT> ResultT execute(
        @NonNull RequestT request,
        @NonNull GenericType<ResultT> resultType
    ) {
        return null;
    }

    @NonNull
    @Override
    public CompletionStage<Void> closeFuture() {
        return null;
    }

    @NonNull
    @Override
    public CompletionStage<Void> closeAsync() {
        return null;
    }

    @NonNull
    @Override
    public CompletionStage<Void> forceCloseAsync() {
        return null;
    }
}
  1. Set up no-op driver context
public class NoOpDriverContext implements DriverContext {


    @NonNull
    @Override
    public String getSessionName() {
        return null;
    }

    @NonNull
    @Override
    public DriverConfig getConfig() {
        return null;
    }

    @NonNull
    @Override
    public DriverConfigLoader getConfigLoader() {
        return null;
    }

    @NonNull
    @Override
    public Map<String, LoadBalancingPolicy> getLoadBalancingPolicies() {
        return null;
    }

    @NonNull
    @Override
    public Map<String, RetryPolicy> getRetryPolicies() {
        return null;
    }

    @NonNull
    @Override
    public Map<String, SpeculativeExecutionPolicy> getSpeculativeExecutionPolicies() {
        return null;
    }

    @NonNull
    @Override
    public TimestampGenerator getTimestampGenerator() {
        return null;
    }

    @NonNull
    @Override
    public ReconnectionPolicy getReconnectionPolicy() {
        return null;
    }

    @NonNull
    @Override
    public AddressTranslator getAddressTranslator() {
        return null;
    }

    @NonNull
    @Override
    public Optional<AuthProvider> getAuthProvider() {
        return Optional.empty();
    }

    @NonNull
    @Override
    public Optional<SslEngineFactory> getSslEngineFactory() {
        return Optional.empty();
    }

    @NonNull
    @Override
    public RequestTracker getRequestTracker() {
        return null;
    }

    @NonNull
    @Override
    public RequestThrottler getRequestThrottler() {
        return null;
    }

    @NonNull
    @Override
    public NodeStateListener getNodeStateListener() {
        return null;
    }

    @NonNull
    @Override
    public SchemaChangeListener getSchemaChangeListener() {
        return null;
    }

    @NonNull
    @Override
    public ProtocolVersion getProtocolVersion() {
        return null;
    }

    @NonNull
    @Override
    public CodecRegistry getCodecRegistry() {
        return new CodecRegistry() {
            @NonNull
            @Override
            public <JavaTypeT> TypeCodec<JavaTypeT> codecFor(
                @NonNull DataType cqlType,
                @NonNull GenericType<JavaTypeT> javaType
            ) {
                return null;
            }

            @NonNull
            @Override
            public <JavaTypeT> TypeCodec<JavaTypeT> codecFor(@NonNull DataType cqlType) {
                return null;
            }

            @NonNull
            @Override
            public <JavaTypeT> TypeCodec<JavaTypeT> codecFor(@NonNull GenericType<JavaTypeT> javaType) {
                return null;
            }

            @NonNull
            @Override
            public <JavaTypeT> TypeCodec<JavaTypeT> codecFor(@NonNull DataType cqlType, @NonNull JavaTypeT value) {
                return null;
            }

            @NonNull
            @Override
            public <JavaTypeT> TypeCodec<JavaTypeT> codecFor(@NonNull JavaTypeT value) {
                return null;
            }
        };
    }
}

And that should allow you to start your app even when Cassandra is down.

Upvotes: 1

Erick Ramirez
Erick Ramirez

Reputation: 16323

This isn't possible since your app has a dependency on being connected to the database before it can start.

The beans can't be instantiated unless the driver is connected to Cassandra. Cheers!

Upvotes: 2

Related Questions