You have setup a causal cluster and want to run SDN with OGM against it. Learn which dependencies are required and how to test it.

Please find the whole example here: sdn-and-causal-cluster.

1. What dependencies are needed and how you can use it?

Neo4j-OGM prior to 3.1.8 has a bug that leads to problems in some concurrent scenarios. Please use at least 3.1.8.

If you only want to connect against your causal cluster, you only need the official Neo4j Java Driver. Configure your cluster URL like this:

spring.data.neo4j.uri=bolt+routing://your-cluster-member:7687

We recommend using a named DNS entry as entrypoint to your cluster. However, in an upcoming Spring Boot version, you’ll be able to configure multiple URIs as well.

If you want to use our Bookmark support, you’ll need additional dependencies as described in Listing 1

Listing 1. pom.xml, Dependencies needed to use @Bookmark
<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
</dependency>

2. General remarks about routing from SDN

In the Spring world, the readOnly attribute on @Transactional is usually passed on onto the underlying technology as a hint. This is equally true for JDBC and JPA as well as for Bolt and OGM. In standalone mode of Bolt it doesn’t do anything. In bolt+routing however, the driver will direct queries issued inside those transactions into readers.

3. How to use @Bookmark

If you want to apply a "read your own write scenario" through @Bookmark, you should orchestrate the calls to a repository class from an outer service layer.

Listing 2. ThingService.java, orchestrating calls
@Service
@Retry(name = "neo4j")
public class ThingService {

        private final ThingRepository thingRepository;

        @Transactional
        public Thing newThing(long i) {
                Thing thing = new Thing(i);
                return this.thingRepository.save(thing);
        }
}

And the corresponding read in the same class:

Listing 3. ThingService.java, orchestrating calls
@Transactional(readOnly = true)
@UseBookmark
public List<Thing> findLatestThings(Long sequence) {
        return thingRepository.findAllBySequenceNumberGreaterThanEqual(sequence);
}

When called from a controller for example the writing call will store the bookmark retrieved from the server, so that the reading call can access it’s own writes:

Listing 4. ThingController.java, using the server
@RestController
public class ThingController {

        private final ThingService thingService;

        private final AtomicLong sequence;

        @PostMapping("/new")
        public Thing newThing() {

                Thing newThing = this.thingService.newThing(sequence.incrementAndGet());
                List<Thing> readThings = this.thingService
                        .findLatestThings(newThing.getSequenceNumber());
                Assert.isTrue(readThings.contains(newThing), "Did not read my own write :(");

                return newThing;
        }
}

As you see, the controller asserts whether it could actually read it’s own write.

That can be also tested under load. I have a created a custom JUnit 5 extension to easily bring up a causal cluster during test, find it here JUnit Jupiter Causal Cluster Testcontainer extension.

Listing 5. DemoApplicationTest.java
@NeedsCausalCluster (1)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (2)
class DemoApplicationTest {

        @CausalCluster (3)
        private static String clusterUri;

        @DynamicPropertySource
        static void neo4jProperties(DynamicPropertyRegistry registry) {
                registry.add("spring.data.neo4j.uri", () -> clusterUri); (4)
                registry.add("spring.data.neo4j.username", () -> "neo4j");
                registry.add("spring.data.neo4j.password", () -> "password");
        }

        @Autowired
        TestRestTemplate restTemplate; (5)

        @RepeatedTest(10)
        void useBookmarkUnderLoad() throws InterruptedException {

                int numberOfParallelTests = 10;

                Callable<Thing> callableRequest = () -> {
                        ResponseEntity<Thing> response = restTemplate
                                .postForEntity("/new", new HttpEntity<Void>(null, null), Thing.class);
                        if (response.getStatusCode() != HttpStatus.OK) {
                                throw new RuntimeException(response.getStatusCode().toString());
                        }
                        return response.getBody();
                };

                ExecutorService executor = Executors.newCachedThreadPool();
                List<Future<Thing>> calledRequests = executor.invokeAll(IntStream.range(0, numberOfParallelTests)
                        .mapToObj(i -> callableRequest).collect(toList()));
                try {
                        calledRequests.forEach(request -> {
                                try {
                                        request.get();
                                } catch (InterruptedException e) {
                                } catch (ExecutionException e) {
                                        Assertions.fail("At least one request failed " + e.getMessage());
                                }
                        });
                } finally {
                        executor.shutdown();
                }
        }
}
1 This informs JUnit 5 that we need a causal cluster. Numbers of leader and followers are configurable.
2 This brings a up a full Spring Environment on a random port, we want load tests so mocks won’t do
3 This is the injection point for the entry point to the cluster
4 This configures an initializer that is needed to pass the cluster URL to the SDN with OGM
5 The TestRestTemplate knows about the random port from 2.