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
@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.
See ThingService:
@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:
@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:
@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.
@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. |