Spring R2dbc: Is there are way to get constant stream from postgresql database and process them? - spring-webflux

I want to fetch records for newly created records in a table in postgresql as a live/continuous stream. Is it possible to use using spring r2dbc? If so what options do I have?
Thanks

You need to use pg_notify and start to listing on it. Any change that you want to see should be wrapped in simple trigger that will send message to pg_notify.
I have an example of this on my github, but long story short:
prepare function and trigger:
CREATE OR REPLACE FUNCTION notify_member_saved()
RETURNS TRIGGER
AS $$
BEGIN
PERFORM pg_notify('MEMBER_SAVED', row_to_json(NEW)::text);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER member_saved_trigger
AFTER INSERT OR UPDATE
ON members
FOR EACH ROW
EXECUTE PROCEDURE notify_member_saved();
In java code prepare listener
#Service
#RequiredArgsConstructor
#Slf4j
class NotificationService {
private final ConnectionFactory connectionFactory;
private final Set<NotificationTopic> watchedTopics = Collections.synchronizedSet(new HashSet<>());
#Qualifier("postgres-event-mapper")
private final ObjectMapper objectMapper;
private PostgresqlConnection connection;
#PreDestroy
private void preDestroy() {
this.getConnection().close().subscribe();
}
private PostgresqlConnection getConnection() {
if(connection == null) {
synchronized(NotificationService.class) {
if(connection == null) {
try {
connection = Mono.from(connectionFactory.create())
.cast(Wrapped.class)
.map(Wrapped::unwrap)
.cast(PostgresqlConnection.class)
.toFuture().get();
} catch(InterruptedException e) {
throw new RuntimeException(e);
} catch(ExecutionException e) {
throw new RuntimeException(e);
}
}
}
}
return this.connection;
}
public <T> Flux<T> listen(final NotificationTopic topic, final Class<T> clazz) {
if(!watchedTopics.contains(topic)) {
executeListenStatement(topic);
}
return getConnection().getNotifications()
.log("notifications")
.filter(notification -> topic.name().equals(notification.getName()) && notification.getParameter() != null)
.handle((notification, sink) -> {
final String json = notification.getParameter();
if(!StringUtils.isBlank(json)) {
try {
sink.next(objectMapper.readValue(json, clazz));
} catch(JsonProcessingException e) {
log.error(String.format("Problem deserializing an instance of [%s] " +
"with the following json: %s ", clazz.getSimpleName(), json), e);
Mono.error(new DeserializationException(topic, e));
}
}
});
}
private void executeListenStatement(final NotificationTopic topic) {
getConnection().createStatement(String.format("LISTEN \"%s\"", topic)).execute()
.doOnComplete(() -> watchedTopics.add(topic))
.subscribe();
}
public void unlisten(final NotificationTopic topic) {
if(watchedTopics.contains(topic)) {
executeUnlistenStatement(topic);
}
}
private void executeUnlistenStatement(final NotificationTopic topic) {
getConnection().createStatement(String.format("UNLISTEN \"%s\"", topic)).execute()
.doOnComplete(() -> watchedTopics.remove(topic))
.subscribe();
}
}
start listiong from controller
#GetMapping("/events")
public Flux<ServerSentEvent<Object>> listenToEvents() {
return Flux.merge(listenToDeletedItems(), listenToSavedItems())
.map(o -> ServerSentEvent.builder()
.retry(Duration.ofSeconds(4L))
.event(o.getClass().getName())
.data(o).build()
);
}
#GetMapping("/unevents")
public Mono<ResponseEntity<Void>> unlistenToEvents() {
unlistenToDeletedItems();
unlistenToSavedItems();
return Mono.just(
ResponseEntity
.status(HttpStatus.I_AM_A_TEAPOT)
.body(null)
);
}
private Flux<Member> listenToSavedItems() {
return this.notificationService.listen(MEMBER_SAVED, Member.class);
}
private void unlistenToSavedItems() {
this.notificationService.unlisten(MEMBER_SAVED);
}
but remember that if something broke then you lost pg_notify events for some time so it is for non-mission-citical solutions.

Related

Issue with reactive nested flatMap calls

I need to perform an operation on two Monos. The difficulty is that one depend on the result of the other.
Let me explain:
I have a Mono<User> (I get that from a ServerRequest; User is a POJO).
I need to be able to extract the user email from the above Mono and pass it to the UserRepository in order to check whether the email already exists in DB.
If the user already exists I will throw a 400 error; otherwise, I will save the user contained in the ServerRequest.
Here is what I have tried in my handler:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest) {
return serverRequest.bodyToMono(User.class)
.flatMap(user -> userRepository
.findByEmail(user.getEmail())
.flatMap(foundUser -> {
if (foundUser != null) {
System.out.println("found:" + foundUser);
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already exists");
} else {
System.out.println("creating" + user);
return status(CREATED).contentType(APPLICATION_JSON).body(userRepository.save(user), User.class);
}
}));
}
User:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User {
#Id
private Integer id;
#Size(min = 2)
private String firstName;
#Size(min = 2)
private String lastName;
#Email
private String email;
}
UserRepository:
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
#Query("select id, first_name, last_name, email from user u where u.email = :email")
Mono<User> findByEmail(String email);
}
However, It seems there's an issue with my subscription: none of my System.out.println are called when the endpoint is called. Can someone please help?
edit 1: Here is the router calling the above handler method:
#Configuration
public class UserRouter {
#Bean
public RouterFunction<ServerResponse> route(UserHandler userHandler) {
return RouterFunctions.route()
.GET("/api/user", accept(APPLICATION_JSON), userHandler::getUsers)
.POST("/api/sign-up", accept(APPLICATION_JSON), userHandler::saveUser)
.build();
}
}
The issue here is that you expect a null when the user is not found, however, in reactive streams null is invalid. Instead, reactive streams have a dedicated empty state and dedicated operators to handle the empty case.
In your example you could do the following:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest)
{
return serverRequest.bodyToMono(User.class)
.flatMap(this::createUserIfNotExists);
}
private Mono<ServerResponse> createUserIfNotExists(User user)
{
return userRepository.findByEmail(user.getEmail())
.hasElement()
.flatMap(foundUser ->
{
if (foundUser)
{
System.out.println("found user");
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"Email already exists");
} else
{
System.out.println("creating user");
return status(CREATED).contentType(APPLICATION_JSON)
.body(userRepository.save(user), User.class);
}
});
}
or another alternative:
public Mono<ServerResponse> saveUser(ServerRequest serverRequest)
{
return serverRequest.bodyToMono(User.class)
.flatMap(this::createUserIfNotExists);
}
private Mono<ServerResponse> createUserIfNotExists(User user)
{
return userRepository.findByEmail(user.getEmail())
.flatMap(foundUser ->
status(BAD_REQUEST)
.contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject("User already exists."))
)
.switchIfEmpty(
userRepository.save(user)
.flatMap(newUser -> status(CREATED).contentType(APPLICATION_JSON)
.body(BodyInserters.fromObject(newUser)))
);
}

WebTestClient used multiple times returns empty body sometimes

not sure, why this could be an issue, but I can't stabilize my unit-tests.
Here some snippets from my testclass:
#SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.main.web-application-type=reactive" })
#RunWith(SpringRunner.class)
#TestPropertySource(locations = "classpath:application-test.properties")
public class SolrControllerV1Test {
#Inject
ApplicationContext context;
#LocalServerPort
int port;
private WebTestClient client;
#TestConfiguration
static class TestConfig {
#Bean
public TestingAuthenticationProvider testAuthentiationManager() {
return new TestingAuthenticationProvider();
}
#Bean
public SecurityWebFilterChain securityConfig(ServerHttpSecurity http, ReactiveAuthenticationManager authenticationManager) {
AuthenticationWebFilter webFilter = new AuthenticationWebFilter(authenticationManager);
return http.addFilterAt(webFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange()
.anyExchange()
.authenticated()
.and()
.build();
}
}
#Before
public void setUp() {
this.client = WebTestClient.bindToApplicationContext(context).configureClient().responseTimeout(Duration.ofDays(1L)).baseUrl("http://localhost:" + port).build();
}
private void defaultCheck(ResponseSpec spec) {
spec.expectStatus().isOk().expectBody().jsonPath("$.response.numFound").hasJsonPath();
}
#Test
#WithMockUser(roles = { "ADMIN" })
public void simpleUsrSelect() throws Exception {
ResponseSpec spec = this.client.get().uri("/" + serviceVersion + "/usr/select?q=*:*&fq=*:*&fl=USRTYP,USRKEY,USRCID&rows=1&start=10&sort=last_update desc").exchange();
defaultCheck(spec);
}
#Test
#WithMockUser(roles = { "ADMIN" })
public void simpleCvdSelect() throws Exception {
ResponseSpec spec = this.client.get().uri("/" + serviceVersion + "/cvd/select?q=*:*&rows=10000").exchange();
defaultCheck(spec);
}
.
.
.
}
There are some more unit-tests there, some of which are long running (>1sec). If I have enough unit-tests in the class (~5-8), of which 1 or 2 are taking a bit longer, the unit-tests start to break. This looks like a thread safety issue, but I don't know, what I'm doing wrong. Any ideas?
EDIT
Here the Server Part that made trouble:
#PreAuthorize("hasAnyRole('ADMIN','TENANT')")
public Mono<ServerResponse> select(ServerRequest request) {
return request.principal().flatMap((principal) -> {
return client.get().uri(f -> {
URI u = f.path(request.pathVariable("collection")).path("/select/").queryParams(
queryModifier.modify(principal, request.pathVariable("collection"), request.queryParams())
.onErrorMap(NoSuchFieldException.class, t -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Collection not found"))
.block()).build();
return u;
})
.exchange()
.flatMap((ClientResponse mapper) -> {
return ServerResponse.status(mapper.statusCode())
.headers(c -> mapper.headers().asHttpHeaders().forEach((name, value) -> c.put(name, value)))
.body(mapper.bodyToFlux(DataBuffer.class), DataBuffer.class);
})
.doOnError(t -> handleAuthxErrors(t, principal, request.uri()));
});
}
If I add a publishOn(Schedulers.elastic) right after the .exchange() part, it seems to be working. Since this is trial&error, and I don't really understand why the publishOn fixes the problem, does anybody else know? I'm not even sure, whether using springs reactive Webclient is blocking in this case, or not...
Thanks, Henning

Is there a standard way to package many Restlets into a single Restlet?

I have a situation where the application developers and the framework provider are not the people. As a framework provider, I would like to be able to hand the developers what looks like a single Filter, but is in fact a chain of standard Filters (such as authentication, setting up invocation context, metrics, ++).
I don't seem to find this functionality in the standard library, but maybe there is an extension with it.
Instead of waiting for an answer, I went ahead with my own implementation and sharing here if some needs this.
/**
* Composes an array of Restlet Filters into a single Filter.
*/
public class ComposingFilter extends Filter
{
private final Filter first;
private final Filter last;
public ComposingFilter( Filter... composedOf )
{
Objects.requireNonNull( composedOf );
if( composedOf.length == 0 )
{
throw new IllegalArgumentException( "Filter chain can't be empty." );
}
first = composedOf[ 0 ];
Filter prev = first;
for( int i = 1; i < composedOf.length; i++ )
{
Filter next = composedOf[ i ];
prev.setNext( next );
prev = next;
}
last = composedOf[ composedOf.length - 1 ];
}
#Override
protected int doHandle( Request request, Response response )
{
if( first != null )
{
first.handle( request, response );
Response.setCurrent( response );
if( getContext() != null )
{
Context.setCurrent( getContext() );
}
}
else
{
response.setStatus( Status.SERVER_ERROR_INTERNAL );
getLogger().warning( "The filter " + getName() + " was executed without a next Restlet attached to it." );
}
return CONTINUE;
}
#Override
public synchronized void start()
throws Exception
{
if( isStopped() )
{
first.start();
super.start();
}
}
#Override
public synchronized void stop()
throws Exception
{
if( isStarted() )
{
super.stop();
first.stop();
}
}
#Override
public Restlet getNext()
{
return last.getNext();
}
#Override
public void setNext( Class<? extends ServerResource> targetClass )
{
last.setNext( targetClass );
}
#Override
public void setNext( Restlet next )
{
last.setNext( next );
}
}
NOTE: Not tested yet.

Do an action when an error occurs RxJava

I need to create a folder when it doesn't exist. In my case, the only way to do so is to capture the error and handle it to create the folder wanted.
But all i can find is
public static Observable<Boolean> folderExists(final Context context, final String targetPath, final String currentpath) {
Application application = Application.get(context);
//i browse the folder to get all the items
return browseFolderObservable(context,currentpath)
.subscribeOn(application.defaultSubscribeScheduler())
.doOnError(new Action1<Throwable>() {
#Override
public void call(Throwable throwable) {
BsSdkLog.d("Error no file found");
}
})
.map(new Func1<ArrayList<Item>, Boolean>() {
#Override
public Boolean call(ArrayList<Item> items) {
if(items.isEmpty()) {
BsSdkLog.d(" No items");
return false;
}else {
for(int i=0;i<items.size();i++)
{
Item item=items.get(i);
BsSdkLog.d(item.toString());
}
BsSdkLog.d("Right-here");
return true;
}
}
});
}
I want to call the method that i have that creates the folder when the error occurs but i don't know how to do that.
I'm new to this so i'd really appreciate the help
Thanks
The basic principe looks like this. I used the Java NIO library for testing.
The method 'createFolder' just wraps creating a folder. The test 'name' invokes the Single and checks for an Exception. If it is an IOException it will return a fallback value. You may do something different in there. You just provide a fallback single. If it is an error different from IOException, it will return the error.
#Test
void name() throws Exception {
final String TO_CREATE = "/home/sergej/Downloads/Wurstbrot";
this.createFolder(TO_CREATE)
.onErrorResumeNext(throwable -> { // handle Exception:
// Validate Exception
if (throwable instanceof IOException) {
// Return fallback
return Single.just(Paths.get("/home/sergej/Downloads/"));
}
return Single.error(throwable);
})
.test()
.await()
.assertValueCount(1)
.assertValue(path -> path.endsWith(TO_CREATE))
.assertNoErrors();
}
private Single<Path> createFolder(String p) {
return Single.defer(() -> { // may throw some IOException
Path path = Paths.get(p);
if (!Files.exists(path)) {
Path createdDirectory = Files.createDirectory(path); // will throw if already exists
return Single.just(createdDirectory);
}
// Or just return Path, because it already exists???
return Single.error(new IOException("Already exists"));
});
}

Oracle Coherence index not working with ContainsFilter query

I've added an index to a cache. The index uses a custom extractor that extends AbstractExtractor and overrides only the extract method to return a List of Strings. Then I have a ContainsFilter which uses the same custom extractor that looks for the occurence of a single String in the List of Strings. It does not look like my index is being used based on the time it takes to execute my test. What am I doing wrong? Also, is there some debugging I can switch on to see which indices are used?
public class DependencyIdExtractor extends AbstractExtractor {
/**
*
*/
private static final long serialVersionUID = 1L;
#Override
public Object extract(Object oTarget) {
if (oTarget == null) {
return null;
}
if (oTarget instanceof CacheValue) {
CacheValue cacheValue = (CacheValue)oTarget;
// returns a List of String objects
return cacheValue.getDependencyIds();
}
throw new UnsupportedOperationException();
}
}
Adding the index:
mCache = CacheFactory.getCache(pCacheName);
mCache.addIndex(new DependencyIdExtractor(), false, null);
Performing the ContainsFilter query:
public void invalidateByDependencyId(String pDependencyId) {
ContainsFilter vContainsFilter = new ContainsFilter(new DependencyIdExtractor(), pDependencyId);
#SuppressWarnings("rawtypes")
Set setKeys = mCache.keySet(vContainsFilter);
mCache.keySet().removeAll(setKeys);
}
I solved this by adding a hashCode and equals method implementation to the DependencyIdExtractor class. It is important that you use exactly the same value extractor when adding an index and creating your filter.
public class DependencyIdExtractor extends AbstractExtractor {
/**
*
*/
private static final long serialVersionUID = 1L;
#Override
public Object extract(Object oTarget) {
if (oTarget == null) {
return null;
}
if (oTarget instanceof CacheValue) {
CacheValue cacheValue = (CacheValue)oTarget;
return cacheValue.getDependencyIds();
}
throw new UnsupportedOperationException();
}
#Override
public int hashCode() {
return 1;
}
#Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof DependencyIdExtractor) {
return true;
}
return false;
}
}
To debug Coherence indices/queries, you can generate an explain plan similar to database query explain plans.
http://www.oracle.com/technetwork/tutorials/tutorial-1841899.html
#SuppressWarnings("unchecked")
public void invalidateByDependencyId(String pDependencyId) {
ContainsFilter vContainsFilter = new ContainsFilter(new DependencyIdExtractor(), pDependencyId);
if (mLog.isTraceEnabled()) {
QueryRecorder agent = new QueryRecorder(RecordType.EXPLAIN);
Object resultsExplain = mCache.aggregate(vContainsFilter, agent);
mLog.trace("resultsExplain = \n" + resultsExplain + "\n");
}
#SuppressWarnings("rawtypes")
Set setKeys = mCache.keySet(vContainsFilter);
mCache.keySet().removeAll(setKeys);
}