Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat : handle exceptional scenario #1642

Merged
merged 3 commits into from
Jan 12, 2025
Merged

feat : handle exceptional scenario #1642

merged 3 commits into from
Jan 12, 2025

Conversation

rajadilipkolli
Copy link
Owner

@rajadilipkolli rajadilipkolli commented Jan 12, 2025

Summary by CodeRabbit

  • Chores

    • Updated project build configuration with improved test coverage and reporting tools.
    • Reorganized package structure for better code organization.
  • New Features

    • Added input validation for notification channels.
    • Enhanced notification handling with improved logging and error management.
    • Introduced integration tests for the notification API.
    • Enabled detailed problem reports in the WebFlux framework.
  • Bug Fixes

    • Improved error handling in notification listener and controller.
    • Added retry mechanism for notification subscriptions.
  • Documentation

    • Added documentation comments for configuration properties.

@rajadilipkolli rajadilipkolli self-assigned this Jan 12, 2025
Copy link
Contributor

coderabbitai bot commented Jan 12, 2025

Walkthrough

This pull request introduces significant changes to a PostgreSQL notification listener application, focusing on enhancing test configuration and coverage reporting, as well as restructuring packages and improving validation. Key modifications include the relocation of NotificationProperties to a new config package, the addition of input validation for channel names, and updates to the notification handling mechanism. The Maven configuration has been extensively revised to incorporate the JaCoCo code coverage tool and improve test reporting capabilities.

Changes

File Change Summary
pom.xml Added JaCoCo code coverage plugin, updated test configuration, and introduced new properties for test reporting and coverage.
NotifyListenPGApplication.java Updated import path for NotificationProperties from listener to config package.
NotificationProperties.java Moved package from listener to config, added documentation comments.
NotificationController.java Added @Validated annotation, modified method access modifiers, and added regex validation for channel names.
NotificationListener.java Renamed variables, updated notification sink, and improved error handling and logging.
NotificationRequest.java Added regex pattern validation for channel names.
Notifier.java Updated constructor to include NotificationListener, enhanced notification process logging.
ApplicationIntTest.java Introduced a new integration test class with a method to test the /api/notify endpoint.
application.properties Added property to enable detailed problem reports in WebFlux.

Possibly related PRs

  • feat : Implement Notify and Listen Support #1639: Implements PostgreSQL NOTIFY and LISTEN features, which are directly related to the modifications in the main PR that enhance test configuration and coverage reporting.
  • feat : connecting to db using jndi #1250: Involves changes to the pom.xml file, including configurations for the JaCoCo code coverage tool, which may relate to the overall testing and coverage aspects of the main PR's implementation.
  • feat : adds R2DBC Test #1627: Adds tests for R2DBC functionality, which is relevant to the main PR as it implements R2DBC features for notifications.

Suggested labels

component: r2dbc

Poem

🐰 A Rabbit's Ode to Code Refinement 🔍
In packages neat, we dance and we hop
Validations spring, errors come to a stop
Coverage reports, a JaCoCo delight
Refactoring code with technical might
Notifications flow, clean and precise!

Finishing Touches

  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (11)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/config/NotificationProperties.java (1)

12-18: Consider adding @SInCE tag to the documentation.

The documentation is clear and comprehensive. Consider adding a @since tag to track when these properties were introduced.

 /*
  * Configuration properties for PostgreSQL notification listener.
  *
  * @param channelName Name of the notification channel to listen to
  * @param listenerEnabled Flag to enable/disable the notification listener
  * @param connectionPoolSize Size of the R2DBC connection pool (1-100)
+ * @since 1.0.0
  */
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java (1)

27-30: Add a custom error message for validation.

While the regex pattern [a-zA-Z0-9_]+ is a good choice for validating channel names, consider adding a custom message to provide clear feedback when validation fails.

-    @PathVariable @Pattern(regexp = "[a-zA-Z0-9_]+") String channel) {
+    @PathVariable @Pattern(
+        regexp = "[a-zA-Z0-9_]+",
+        message = "Channel name must contain only letters, numbers, and underscores"
+    ) String channel) {
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2)

49-50: Consider making rate limiting parameters configurable.

The rate limit (100) and delay (1 second) are hardcoded. Consider moving these to NotificationProperties for better configurability.

+    // In NotificationProperties.java
+    private final int rateLimitSize = 100;
+    private final Duration delayDuration = Duration.ofSeconds(1);

     // In NotificationListener.java
-                .limitRate(100)
-                .delayElements(Duration.ofSeconds(1))
+                .limitRate(notificationProperties.rateLimitSize())
+                .delayElements(notificationProperties.delayDuration())

42-42: Consider extracting retry configuration to avoid duplication.

The retry configuration (3 attempts, 1s backoff) is duplicated. Consider extracting it to a constant or configuration property.

+    private static final Retry RETRY_STRATEGY = Retry.backoff(3, Duration.ofSeconds(1));

-                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
+                .retryWhen(RETRY_STRATEGY)

-                .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
+                .retryWhen(RETRY_STRATEGY)

Also applies to: 63-63

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/NotificationRequest.java (1)

Line range hint 6-8: Consider PostgreSQL-specific channel name requirements.

While the current validation is secure, it might be too restrictive for PostgreSQL channel names. PostgreSQL allows additional characters in identifiers.

Consider:

  1. Updating the pattern to match PostgreSQL identifier rules
  2. Adding a custom error message
  3. Removing redundant @NotBlank for channel as the pattern already prevents empty strings
 public record NotificationRequest(
-        @NotBlank @Pattern(regexp = "[a-zA-Z0-9_]+") String channel,
+        @Pattern(
+            regexp = "^[a-zA-Z_][a-zA-Z0-9_$]*$",
+            message = "Channel name must start with a letter or underscore and contain only letters, numbers, underscores, or $ characters"
+        ) String channel,
         @NotBlank String message) {}
r2dbc/boot-r2dbc-notify-listen/pom.xml (3)

129-149: Good Git information tracking setup!

Consider adding these useful Git properties for better deployment tracking:

  • git.commit.time
  • git.build.time
 <includeOnlyProperties>
     <includeOnlyProperty>^git.branch$</includeOnlyProperty>
     <includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
     <includeOnlyProperty>^git.commit.user.name$</includeOnlyProperty>
     <includeOnlyProperty>^git.commit.message.full$</includeOnlyProperty>
+    <includeOnlyProperty>^git.commit.time$</includeOnlyProperty>
+    <includeOnlyProperty>^git.build.time$</includeOnlyProperty>
 </includeOnlyProperties>

150-191: Well-organized test execution setup!

Consider enabling parallel test execution to reduce build time:

 <configuration>
     <runOrder>alphabetical</runOrder>
     <reportsDirectory>${junit.utReportFolder}</reportsDirectory>
+    <parallel>classes</parallel>
+    <threadCount>4</threadCount>
     <excludes>

194-263: Robust code coverage configuration!

Consider adding additional coverage metrics for more comprehensive quality assurance:

 <limits>
     <limit>
         <counter>COMPLEXITY</counter>
         <value>COVEREDRATIO</value>
         <minimum>${jacoco.minimum.coverage}</minimum>
     </limit>
+    <limit>
+        <counter>LINE</counter>
+        <value>COVEREDRATIO</value>
+        <minimum>${jacoco.minimum.coverage}</minimum>
+    </limit>
+    <limit>
+        <counter>BRANCH</counter>
+        <value>COVEREDRATIO</value>
+        <minimum>${jacoco.minimum.coverage}</minimum>
+    </limit>
 </limits>
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (3)

29-29: Adjust log level for successful operations to reduce log noise.

Using log.info for each successful operation might clutter the logs, especially in a production environment. Consider changing the log level to debug to reduce verbosity.

Apply this diff to adjust the log level:

-                        .doOnSuccess(postgresqlResult -> log.info(postgresqlResult.toString()))
+                        .doOnSuccess(postgresqlResult -> log.debug(postgresqlResult.toString()))

30-39: Improve code readability by extracting the notification sending logic into a separate method.

Extracting the inner logic into a dedicated method enhances readability and makes the code easier to maintain.

Example:

private Mono<ServerResponse> sendNotification(NotificationRequest notificationRequest) {
    log.debug("Channel {} registered, sending notification", notificationRequest.channel());
    return databaseClient
        .sql("SELECT pg_notify(:channel, :message)")
        .bind("channel", notificationRequest.channel())
        .bind("message", notificationRequest.message())
        .fetch()
        .rowsUpdated()
        .flatMap(rowsUpdated -> ServerResponse.ok().bodyValue(rowsUpdated))
        .onErrorResume(error -> {
            log.error("Failed to send notification", error);
            return ServerResponse.status(500).bodyValue("Failed to send notification");
        });
}

Then update the code:

 return req.bodyToMono(NotificationRequest.class)
         .flatMap(notificationRequest -> notificationListener
             .listenTo(notificationRequest.channel())
             .doOnSuccess(postgresqlResult -> log.debug(postgresqlResult.toString()))
-            .then(Mono.defer(() -> {
-                log.debug("Channel {} registered, sending notification", notificationRequest.channel());
-                return databaseClient
-                    .sql("SELECT pg_notify(:channel, :message)")
-                    .bind("channel", notificationRequest.channel())
-                    .bind("message", notificationRequest.message())
-                    .fetch()
-                    .rowsUpdated()
-                    .flatMap(rowsUpdated -> ServerResponse.ok().bodyValue(rowsUpdated))
-                    .onErrorResume(error -> {
-                        log.error("Failed to send notification", error);
-                        return ServerResponse.status(500).bodyValue("Failed to send notification");
-                    });
-            })))
+            .then(Mono.defer(() -> sendNotification(notificationRequest))))
         .onErrorResume(error -> {
             log.error("Failed to process notification request", error);
             return ServerResponse.status(500).bodyValue("Failed to process notification request");
         });

40-40: Remove redundant doOnError to prevent double logging of errors.

Since errors are handled in onErrorResume, the doOnError might lead to logging the same error twice.

Apply this diff to remove the redundant error logging:

-                .doOnError(error -> log.error("Failed to send notification", error));
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbd40ea and 763edd4.

📒 Files selected for processing (8)
  • r2dbc/boot-r2dbc-notify-listen/pom.xml (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/NotifyListenPGApplication.java (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/config/NotificationProperties.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/NotificationRequest.java (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (1 hunks)
✅ Files skipped from review due to trivial changes (2)
  • r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/NotifyListenPGApplication.java
🧰 Additional context used
📓 Learnings (3)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/config/NotificationProperties.java (1)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1639
File: r2dbc/boot-r2dbc-notify-listen/src/main/resources/application.properties:1-19
Timestamp: 2025-01-12T11:22:52.010Z
Learning: The NotificationListener in Spring Boot applications can be configured using properties:
- `app.notification.channel-name`: Specifies the default channel to listen to
- `app.notification.listener.enabled`: Controls whether the listener is enabled
- `app.notification.connection-pool-size`: Controls the R2DBC connection pool size
The configuration is bound using @ConfigurationProperties and @EnableConfigurationProperties annotations.
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (1)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1639
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java:45-45
Timestamp: 2025-01-12T11:55:38.018Z
Learning: In PostgreSQL NOTIFY/LISTEN implementation, always ensure that a channel is being listened to before sending notifications to it. Use `listenTo` method to register the channel if it's not already being listened to.
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (1)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#0
File: :0-0
Timestamp: 2025-01-12T10:45:47.987Z
Learning: The NotificationListener service in Spring Boot R2DBC applications should handle PostgreSQL notifications by:
1. Using PostgresqlConnection for low-level notification handling
2. Publishing notifications as both Spring events and reactive streams
3. Managing connection lifecycle with @PostConstruct and @PreDestroy
4. Providing methods to listen to specific channels and get notifications as Flux
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (18)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/config/NotificationProperties.java (4)

1-1: LGTM! Package move improves code organization.

Moving NotificationProperties to the config package better aligns with Spring Boot conventions for configuration classes.


10-11: LGTM! Proper configuration and validation setup.

The @ConfigurationProperties prefix matches the expected configuration structure, and @Validated ensures property validation.


19-20: LGTM! Comprehensive validation constraints.

The validation constraints are well-chosen:

  • @NotBlank ensures a non-empty channel name
  • @NotNull for explicit listener enablement
  • @Min(1) @Max(100) provides reasonable bounds for the connection pool

1-1: Verify package move impact.

Let's verify that all references to NotificationProperties have been updated to use the new package.

✅ Verification successful

Package move verified successfully

All references to NotificationProperties are using the new package path com.example.demo.config. No outdated references found.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining references to the old package
rg "com\.example\.demo\.listener\.NotificationProperties"

# Search for references to the new package to confirm updates
rg "com\.example\.demo\.config\.NotificationProperties"

Length of output: 417

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java (3)

3-5: LGTM! Good addition of validation support.

The addition of validation dependencies and @Validated annotation properly enables method parameter validation, which aligns well with handling exceptional scenarios.

Also applies to: 12-13


22-24: LGTM! Good encapsulation.

The package-private visibility for streamNotifications aligns with the encapsulation approach used in the class.


13-17: Verify the impact of reduced visibility.

The change from public to package-private visibility for both the class and constructor is good for encapsulation. However, please ensure this doesn't break any existing usage from other packages.

✅ Verification successful

Visibility change is safe and recommended

The reduced visibility is perfectly fine because:

  • The class is managed by Spring's component scanning (via @RestController)
  • No direct instantiation or inheritance is found in the codebase
  • REST endpoints remain publicly accessible regardless of class visibility
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any usage of NotificationController from other packages
rg -l "new NotificationController\(" --type java
rg -l "extends NotificationController" --type java
rg -l "@Autowired.*NotificationController" --type java

Length of output: 160


Script:

#!/bin/bash
# Search for any reference to NotificationController
rg "NotificationController" --type java -l

# Check for component scanning configurations
rg "@ComponentScan|@SpringBootApplication" --type java -l

# Look for the actual file to see Spring annotations
fd "NotificationController.java" --type f --exec cat {}

Length of output: 5483

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (4)

3-3: LGTM! Good package organization.

Moving NotificationProperties to a dedicated config package improves the separation of concerns.


23-35: Great improvements to variable naming and sink configuration!

  • Descriptive variable names improve code readability
  • Using multicast sink with backpressure is the correct choice for handling multiple subscribers

41-45: Good addition of retry mechanism with exponential backoff.

The retry mechanism improves resilience against transient failures.


67-72: Good improvement in error handling and return type.

Returning PostgresqlResult provides more information to callers, and the error logging helps with debugging.

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/NotificationRequest.java (2)

4-4: LGTM! Appropriate import for validation.

The import of Pattern from jakarta validation constraints is correct and follows modern standards.


7-7: Verify existing channel names compatibility.

Let's check if existing channel names in the codebase comply with the new validation pattern.

✅ Verification successful

Channel name validation pattern is compatible with existing usage.

The new validation pattern [a-zA-Z0-9_]+ aligns with:

  • Current example usage ("events")
  • Default configuration values
  • PostgreSQL identifier naming best practices
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for channel name usage in the codebase
echo "Searching for channel name declarations and usages..."
rg -g '!*.{class,jar}' -l 'channel.*=.*["'\''].*["'\'']' .

# Search for test cases covering channel name validation
echo "Searching for validation test coverage..."
rg -g '*Test*.java' -l 'channel.*invalid|@Pattern|ValidationException' .

Length of output: 345


Script:

#!/bin/bash
# Search for notification/channel related code
echo "Searching for notification and channel related code..."
rg -g '!*.{class,jar}' -l 'NOTIFY|LISTEN.*channel|setChannel|getChannel' .

# Search for notification related test files
echo "Searching for notification related test files..."
fd -g '*Test*.java' --exec grep -l "Notification" {} \;

# Search for validation related test files
echo "Searching for validation test files..."
fd -g '*Test*.java' --exec grep -l "@Valid|javax.validation|jakarta.validation" {} \;

Length of output: 875


Script:

#!/bin/bash
# Check content of notification related files
echo "Content of NotificationListener.java:"
cat ./r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java

echo -e "\nContent of ContainerConfig.java:"
cat ./r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/common/ContainerConfig.java

echo -e "\nContent of README.md:"
cat ./r2dbc/boot-r2dbc-notify-listen/README.md

Length of output: 8325

r2dbc/boot-r2dbc-notify-listen/pom.xml (3)

20-29: Well-structured test configuration properties!

Good choice setting the minimum coverage threshold to 80%. This provides a solid baseline for maintaining code quality while being achievable.


96-127: Comprehensive code formatting configuration!

Good setup with Spotless plugin:

  • Palantir Java formatter for consistent Java code style
  • SQL formatting for database scripts
  • Automated import optimization

Line range hint 7-8: Verify Spring Boot milestone version stability

You're using Spring Boot 3.4.1 which is a milestone version. Ensure it's stable enough for your needs or consider using the latest stable version.

Run this script to check the latest stable version:

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2)

20-22: Good integration of NotificationListener in the constructor.

Including NotificationListener in the constructor ensures that the notifier can register channels before sending notifications, which aligns with best practices for PostgreSQL NOTIFY/LISTEN implementations.


28-29: Verify that listenTo handles empty or invalid channel names gracefully.

Ensure that the listenTo method in NotificationListener properly handles cases where the channel name might be empty or invalid, to prevent unexpected errors.

Run the following script to check for input validation in the listenTo method:

✅ Verification successful

The implementation has sufficient error handling despite lacking explicit validation

The listenTo method relies on PostgreSQL's built-in channel name validation and includes comprehensive error handling:

  • Errors are caught and logged via doOnError
  • Retry mechanism with backoff is implemented
  • Multiple logging points help track issues

While explicit validation could be added as an enhancement, the current implementation is safe and gracefully handles invalid channel names through database-level validation.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that NotificationListener.listenTo() validates channel names.

# Search for validation logic in the listenTo method.
ast-grep --pattern $'public Mono<$_> listenTo(String channel) {
    if (channel == null || channel.isEmpty()) {
        $$$
    }
    $$$
}' --lang java

Length of output: 153


Script:

#!/bin/bash
# Find the listenTo method implementation
ast-grep --pattern 'listenTo(String $$$) {
  $$$
}'

# Also search for any usage patterns to understand how it's called
rg "listenTo\(" -A 5

Length of output: 3403

Comment on lines 34 to 35
.bind("channel", notificationRequest.channel())
.bind("message", notificationRequest.message())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Validate input parameters to prevent SQL injection.

While parameter binding helps prevent SQL injection, it's important to ensure that the channel and message inputs are validated or sanitized to avoid any potential security risks.

Consider adding validation logic to check the channel and message contents before processing.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (2)

12-13: Rename test method to reflect its purpose

The method name contextLoads is too generic and doesn't describe what's being tested. Consider renaming it to something more specific like shouldSuccessfullyNotifyOnValidChannel.


19-20: Document test data significance

Consider adding a comment explaining why the values "junitChannel" and "junitMessage" were chosen and what the expected behavior is.

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2)

44-44: Review the handling of PostgresqlResult in subscription

In the initialize() method, the subscription to listenTo() logs the postgresqlResult using log.debug(postgresqlResult.toString()). Consider whether logging the postgresqlResult is necessary, as it may not provide meaningful information for debugging. If the result does not need to be processed, you might adjust the subscription to ignore the result or log more relevant details.


50-50: Reevaluate the use of delayElements to reduce latency

The use of .delayElements(Duration.ofSeconds(1)) introduces a 1-second delay between processing each notification. This may cause unnecessary latency in handling notifications. If rate limiting is not required, consider removing this delay to improve the responsiveness of the application.

Apply this diff to remove the delay:

-                    .delayElements(Duration.ofSeconds(1))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 763edd4 and 522438b.

📒 Files selected for processing (5)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/NotificationRequest.java (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/NotificationRequest.java
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java
🧰 Additional context used
📓 Learnings (2)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (1)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#0
File: :0-0
Timestamp: 2025-01-12T10:45:47.987Z
Learning: The NotificationListener service in Spring Boot R2DBC applications should handle PostgreSQL notifications by:
1. Using PostgresqlConnection for low-level notification handling
2. Publishing notifications as both Spring events and reactive streams
3. Managing connection lifecycle with @PostConstruct and @PreDestroy
4. Providing methods to listen to specific channels and get notifications as Flux
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (1)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1639
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java:45-45
Timestamp: 2025-01-12T11:55:38.018Z
Learning: In PostgreSQL NOTIFY/LISTEN implementation, always ensure that a channel is being listened to before sending notifications to it. Use `listenTo` method to register the channel if it's not already being listened to.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (9)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (4)

3-5: LGTM! Well-structured class-level changes.

The addition of proper logging and NotificationListener integration follows best practices and aligns with the requirement to ensure channels are being listened to before sending notifications.

Also applies to: 15-15, 17-18


20-22: LGTM! Clean constructor implementation.

The constructor properly implements dependency injection for both required services.


26-35: LGTM! Robust implementation with proper error handling.

The changes effectively:

  • Ensure channel registration before sending notifications
  • Handle errors appropriately with status 500 responses
  • Use proper logging levels

37-46: Add input validation for channel and message parameters.

While parameter binding helps prevent SQL injection, it's important to validate the input parameters before processing.

Let's verify if input validation has been added elsewhere:

#!/bin/bash
# Search for validation annotations or checks on NotificationRequest
ast-grep --pattern 'class NotificationRequest {
  $$$
  @$_($$$)
  $_($$$) channel() {
    $$$
  }
  $$$
}'
r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (2)

1-10: Well-structured test class with appropriate inheritance!

The class is properly organized and follows testing best practices by extending AbstractIntegrationTest.


12-25: Verify validation implementation

Let's verify the validation constraint implementation and error handling.

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (3)

35-35: Ensure proper backpressure handling with multicast sink

By switching to a multicast sink with .onBackpressureBuffer(), all subscribers will receive the same sequence of notifications, and backpressure is handled by buffering. Make sure that this aligns with the application's requirements and that the buffer memory consumption is acceptable under load. If there's a potential for high throughput, consider specifying a buffer size or using strategies like .onBackpressureLatest().

Would you like to verify the backpressure strategy to prevent potential memory issues under high load?


58-60: Handle emission failures in notificationSink more robustly

When notificationSink.tryEmitNext(notificationEvent) fails, the current implementation logs the error. Consider implementing additional handling for emission failures. Depending on the application's requirements, you might:

  • Retry emitting the notification.
  • Analyze the cause of the failure and take corrective action.
  • Notify an alerting system if critical notifications could be lost.

Would you like assistance in implementing a more robust error handling strategy for emission failures?


85-98: Great job replacing blocking calls with non-blocking alternatives

The previous use of .block() in the cleanup() method has been effectively replaced with a non-blocking approach using .subscribe(). This aligns well with reactive programming best practices and improves the application's responsiveness during shutdown.

return notificationListener.getNotificationStream();
}

@GetMapping(path = "/notifications/{channel}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<NotificationEvent> streamChannelNotifications(@PathVariable String channel) {
Flux<NotificationEvent> streamChannelNotifications(
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, please help me to write IntegrationTest for channel junitChannel of this api to confirm that it has received message send in contextLoads test of ApplicationIntTest

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Here's the integration test for verifying message reception on the junitChannel:

@Test
void shouldReceiveNotificationOnChannel() {
    // Create notification request
    NotificationRequest notificationRequest = new NotificationRequest("junitChannel", "Hello from test!");

    // Subscribe to channel notifications and verify message reception
    webTestClient.get()
            .uri("/notifications/junitChannel")
            .accept(MediaType.TEXT_EVENT_STREAM)
            .exchange()
            .expectStatus().isOk()
            .returnResult(NotificationEvent.class)
            .getResponseBody()
            .as(StepVerifier::create)
            .then(() -> {
                // Send notification after subscribing
                webTestClient.post()
                        .uri("/api/notify")
                        .contentType(MediaType.APPLICATION_JSON)
                        .bodyValue(notificationRequest)
                        .exchange()
                        .expectStatus().isOk();
            })
            .assertNext(event -> {
                assertThat(event.channel()).isEqualTo("junitChannel");
                assertThat(event.message()).isEqualTo("Hello from test!");
            })
            .thenCancel() // Cancel the subscription after verification
            .verify(Duration.ofSeconds(5));
}

This test:

  1. Creates a notification request for the junitChannel
  2. Subscribes to the channel's notifications using WebTestClient
  3. Sends a notification through the /api/notify endpoint
  4. Verifies that the correct notification is received
  5. Cancels the subscription after verification

The test uses StepVerifier to handle the reactive stream and includes a timeout to prevent hanging in case of issues.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, above code is throwing below error, please fix

java.lang.IllegalStateException: Timeout on blocking read for 5000000000 NANOSECONDS

at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:129)
at reactor.core.publisher.Mono.block(Mono.java:1807)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:369)
at com.example.demo.ApplicationIntTest.shouldReceiveNotificationOnChannel(ApplicationIntTest.java:57)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

Caused by: java.util.concurrent.TimeoutException: Timeout on blocking read for 5000000000 NANOSECONDS
... 7 more

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Here's the fixed version of the test:

@Test
void shouldReceiveNotificationOnChannel() {
    // Create notification request
    NotificationRequest notificationRequest = new NotificationRequest("junitChannel", "Hello from test!");
    
    // Start listening to notifications
    FluxExchangeResult<NotificationEvent> result = webTestClient.get()
            .uri("/notifications/junitChannel")
            .accept(MediaType.TEXT_EVENT_STREAM)
            .exchange()
            .expectStatus().isOk()
            .returnResult(NotificationEvent.class);
    
    // Send notification
    webTestClient.post()
            .uri("/api/notify")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(notificationRequest)
            .exchange()
            .expectStatus().isOk()
            .expectBody(Long.class)
            .isEqualTo(1L);

    // Verify the notification is received
    StepVerifier.create(result.getResponseBody().take(1))
            .assertNext(event -> {
                assertThat(event.channel()).isEqualTo("junitChannel");
                assertThat(event.message()).isEqualTo("Hello from test!");
            })
            .verifyComplete();
}

Key changes made:

  1. Separated the subscription and notification sending operations
  2. Used FluxExchangeResult to handle the streaming response
  3. Added take(1) to limit the stream to just the first event
  4. Removed the nested notification sending from within StepVerifier
  5. Added proper verification of the notification response

This should resolve the timeout issue while maintaining the test's effectiveness.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai,

Still receiving below error, should any change be made in main code to fix this?

java.lang.IllegalStateException: Timeout on blocking read for 5000000000 NANOSECONDS

	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:129)
	at reactor.core.publisher.Mono.block(Mono.java:1807)
	at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:369)
	at com.example.demo.ApplicationIntTest.shouldReceiveNotificationOnChannel(ApplicationIntTest.java:57)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
Caused by: java.util.concurrent.TimeoutException: Timeout on blocking read for 5000000000 NANOSECONDS
	... 7 more

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Let's modify both the controller and test code to fix this issue:

  1. First, update the NotificationController to ensure channel readiness:
@GetMapping(path = "/notifications/{channel}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<NotificationEvent> streamChannelNotifications(
        @PathVariable
                @Pattern(
                        regexp = "[a-zA-Z0-9_]+",
                        message = "Channel name must contain only letters, numbers, and underscores")
                String channel) {
    return notificationListener.listenTo(channel)
            .doOnSuccess(result -> log.debug("Successfully subscribed to channel: {}", channel))
            .thenReturn(channel)
            .flatMapMany(ch -> notificationListener.getNotificationStream(ch));
}
  1. And here's the updated test:
@Test
void shouldReceiveNotificationOnChannel() {
    // Create notification request
    NotificationRequest notificationRequest = new NotificationRequest("junitChannel", "Hello from test!");
    
    // Start listening to notifications
    FluxExchangeResult<NotificationEvent> result = webTestClient.get()
            .uri("/notifications/junitChannel")
            .accept(MediaType.TEXT_EVENT_STREAM)
            .exchange()
            .expectStatus().isOk()
            .returnResult(NotificationEvent.class);
    
    // Wait for channel subscription to be established
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }

    // Send notification
    webTestClient.post()
            .uri("/api/notify")
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(notificationRequest)
            .exchange()
            .expectStatus().isOk()
            .expectBody(Long.class)
            .isEqualTo(1L);

    // Verify the notification is received with increased timeout
    StepVerifier.create(result.getResponseBody().take(1))
            .assertNext(event -> {
                assertThat(event.channel()).isEqualTo("junitChannel");
                assertThat(event.message()).isEqualTo("Hello from test!");
            })
            .verifyComplete();
}

The changes:

  1. Added debug logging in the controller to track channel subscription
  2. Added a small delay after subscription to ensure the channel is ready
  3. Used flatMapMany instead of thenMany for better stream handling
  4. Made the subscription chain more explicit with thenReturn

These changes should resolve the timeout issue by ensuring proper channel initialization before sending notifications.


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java:27-27
Timestamp: 2025-01-12T13:47:18.377Z
Learning: When testing PostgreSQL NOTIFY/LISTEN functionality in Spring WebFlux, ensure proper channel initialization by adding a small delay between channel subscription and notification sending to prevent timeout issues.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai, can you once review full code and suggest the fix. debug statements are printed as changed above. Please fix the issue.

2025-01-12T19:21:56.140+05:30 INFO 25452 --- [notify-listen-postgresql] [actor-tcp-nio-1] c.e.demo.listener.NotificationListener : Listening on channel: junitChannel
2025-01-12T19:21:56.140+05:30 DEBUG 25452 --- [notify-listen-postgresql] [actor-tcp-nio-1] c.e.d.listener.NotificationController : Successfully subscribed to channel: junitChannel

java.lang.IllegalStateException: Timeout on blocking read for 5000000000 NANOSECONDS

at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:129)
at reactor.core.publisher.Mono.block(Mono.java:1807)
at org.springframework.test.web.reactive.server.DefaultWebTestClient$DefaultRequestBodyUriSpec.exchange(DefaultWebTestClient.java:369)
at com.example.demo.ApplicationIntTest.shouldReceiveNotificationOnChannel(ApplicationIntTest.java:59)
at java.base/java.lang.reflect.Method.invoke(Method.java:580)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1596)

Caused by: java.util.concurrent.TimeoutException: Timeout on blocking read for 5000000000 NANOSECONDS
... 7 more

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rajadilipkolli Let's fix this by modifying the Notifier class to avoid duplicate channel subscription. Here's the updated code:

@Component
@Slf4j
public class Notifier {

    private final DatabaseClient databaseClient;

    public Notifier(DatabaseClient databaseClient) {
        this.databaseClient = databaseClient;
    }

    public Mono<ServerResponse> notifyData(NotificationRequest notificationRequest) {
        log.debug("Sending notification on channel: {} with message: {}", 
            notificationRequest.channel(), notificationRequest.message());
            
        return sendNotification(notificationRequest)
                .flatMap(rowsUpdated -> ServerResponse.ok()
                        .bodyValue(rowsUpdated))
                .onErrorResume(e -> {
                    log.error("Error sending notification: {}", e.getMessage(), e);
                    return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
                            .bodyValue("Error sending notification: " + e.getMessage());
                });
    }

    private Mono<Long> sendNotification(NotificationRequest notificationRequest) {
        return databaseClient.sql("SELECT pg_notify($1, $2)")
                .bind(0, notificationRequest.channel())
                .bind(1, notificationRequest.message())
                .fetch()
                .rowsUpdated();
    }
}

Key changes:

  1. Removed the NotificationListener dependency
  2. Removed the channel listening step before sending notification
  3. Simplified the notification sending process

The test should now work as expected since:

  1. Controller handles channel subscription
  2. Notifier only sends notifications without trying to listen
  3. No more deadlock in the notification flow

(`・ω・´)ゞ Thoughtfully suggesting this architectural improvement!


✏️ Learnings added
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java:27-27
Timestamp: 2025-01-12T13:55:34.238Z
Learning: When implementing PostgreSQL NOTIFY/LISTEN pattern in Spring WebFlux, separate the concerns:
1. NotificationController/Listener should handle channel subscriptions
2. Notifier should only send notifications without attempting to listen
This prevents deadlocks and timeout issues in integration tests.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
r2dbc/boot-r2dbc-notify-listen/src/main/resources/application.properties (1)

9-10: LGTM! Good addition for enhanced error handling.

Enabling WebFlux problem details is a good practice as it provides standardized error responses following RFC 7807, which helps clients better understand and handle error scenarios.

This configuration will ensure that your error responses include:

  • A type URI identifying the error
  • A title describing the error
  • HTTP status
  • A detailed error message
  • Instance information
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2)

41-61: Consider making retry and rate limit parameters configurable.

The retry mechanism and rate limiting are good additions, but consider moving the magic numbers to configuration:

  • Retry attempts (3)
  • Backoff duration (1 second)
  • Rate limit (100)
 @PostConstruct
 public void initialize() {
     listenTo(notificationProperties.channelName())
-            .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
+            .retryWhen(Retry.backoff(
+                notificationProperties.maxRetries(),
+                notificationProperties.backoffDuration()))
     .subscribe(null, error -> log.error("Fatal error in channel subscription", error));
     
     postgresqlConnection
         .getNotifications()
-        .limitRate(100)
+        .limitRate(notificationProperties.rateLimit())

82-95: Consider enhancing cleanup robustness.

While the cleanup process is improved, consider these enhancements:

  1. Add timeout to prevent hanging during shutdown
  2. Handle case where UNLISTEN fails but connection close succeeds
 @PreDestroy
 public void cleanup() {
     notificationSink.tryEmitComplete();
     
     Mono.from(postgresqlConnection
             .createStatement("UNLISTEN " + notificationProperties.channelName())
             .execute())
         .then(postgresqlConnection.close())
+        .timeout(Duration.ofSeconds(5))
+        .onErrorResume(error -> {
+            log.error("Error during UNLISTEN, attempting to close connection", error);
+            return postgresqlConnection.close();
+        })
         .doOnError(error -> log.error("Error closing connection", error))
         .doOnSuccess(unused -> log.info("Notification listener connection closed"))
         .retryWhen(Retry.backoff(3, Duration.ofSeconds(1)))
         .subscribe(
             unused -> log.info("Notification listener cleanup completed"),
             error -> log.error("Failed to cleanup notification listener", error));
 }
r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (2)

18-31: Enhance test coverage by verifying notification delivery

While the test verifies the API response, it doesn't confirm if the notification was actually delivered. Consider combining this with a notification listener to verify end-to-end functionality.

You can use the approach shown in the disabled test shouldReceiveNotificationOnChannel as a reference.


33-51: Add more validation test cases

The test covers invalid channel name with special characters, but consider adding tests for:

  1. Empty channel name
  2. Empty message
  3. Null values

Example test case for empty channel:

@Test
void shouldRejectEmptyChannelName() {
    this.webTestClient
            .post()
            .uri("/api/notify")
            .accept(MediaType.APPLICATION_JSON)
            .contentType(MediaType.APPLICATION_JSON)
            .bodyValue(new NotificationRequest("", "message"))
            .exchange()
            .expectStatus()
            .isBadRequest()
            .expectBody(ProblemDetail.class)
            .value(problemDetail -> {
                assertThat(problemDetail.getStatus()).isEqualTo(400);
                assertThat(problemDetail.getDetail()).isNotEmpty();
            });
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 522438b and 7985e51.

📒 Files selected for processing (4)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/main/resources/application.properties (1 hunks)
  • r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (1 hunks)
🧰 Additional context used
📓 Learnings (2)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (6)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#0
File: :0-0
Timestamp: 2025-01-12T10:45:47.987Z
Learning: The NotificationListener service in Spring Boot R2DBC applications should handle PostgreSQL notifications by:
1. Using PostgresqlConnection for low-level notification handling
2. Publishing notifications as both Spring events and reactive streams
3. Managing connection lifecycle with @PostConstruct and @PreDestroy
4. Providing methods to listen to specific channels and get notifications as Flux
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationController.java:27-27
Timestamp: 2025-01-12T13:47:18.533Z
Learning: When testing PostgreSQL NOTIFY/LISTEN functionality in Spring WebFlux, ensure proper channel initialization by adding a small delay between channel subscription and notification sending to prevent timeout issues.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java:67-67
Timestamp: 2025-01-12T13:49:28.393Z
Learning: In Spring WebFlux applications with Bean Validation, handle ConstraintViolationException globally using @RestControllerAdvice to convert validation errors into HTTP 400 responses with problem details. Keep validation at the controller level using annotations like @Pattern and @NotBlank, rather than implementing it in the service layer.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java:67-67
Timestamp: 2025-01-12T13:45:58.565Z
Learning: In Spring WebFlux applications, use @RestControllerAdvice instead of @ControllerAdvice for exception handling, and ensure the handler methods return Mono/Flux to maintain the reactive flow. Additionally, use @ResponseStatus to explicitly define the HTTP status code.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java:67-67
Timestamp: 2025-01-12T13:38:22.906Z
Learning: In reactive applications, to handle HTTP 400 (Bad Request) responses, create a custom exception and handle it using a global exception handler (@ControllerAdvice) instead of using ResponseStatusException directly in the service layer.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java:67-67
Timestamp: 2025-01-12T13:34:29.877Z
Learning: In the NotificationListener class, the channel name must be validated to contain only letters, numbers, and underscores (pattern: [a-zA-Z0-9_]+). Invalid channel names should result in a BadRequest (400) response.
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2)
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1642
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java:67-67
Timestamp: 2025-01-12T13:49:28.393Z
Learning: In Spring WebFlux applications with Bean Validation, handle ConstraintViolationException globally using @RestControllerAdvice to convert validation errors into HTTP 400 responses with problem details. Keep validation at the controller level using annotations like @Pattern and @NotBlank, rather than implementing it in the service layer.
Learnt from: rajadilipkolli
PR: rajadilipkolli/my-spring-boot-experiments#1639
File: r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java:45-45
Timestamp: 2025-01-12T11:55:38.018Z
Learning: In PostgreSQL NOTIFY/LISTEN implementation, always ensure that a channel is being listened to before sending notifications to it. Use `listenTo` method to register the channel if it's not already being listened to.
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (6)
r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/listener/NotificationListener.java (3)

3-16: LGTM! Clean import organization.

The relocation of NotificationProperties to the config package improves separation of concerns, and the new imports support the enhanced functionality.


23-35: Great improvements to variable naming and sink configuration!

The changes enhance code clarity and scalability:

  1. Descriptive variable names (postgresqlConnection, notificationProperties)
  2. Using multicast sink allows multiple subscribers to receive notifications

64-69: LGTM! Clean implementation of the listen operation.

The method correctly returns the PostgreSQL result and includes proper error logging. As per the learnings, channel name validation is handled at the controller level.

r2dbc/boot-r2dbc-notify-listen/src/test/java/com/example/demo/ApplicationIntTest.java (1)

53-92: Clarify why the test is disabled

The test appears to be well-structured for verifying end-to-end notification flow. Please add a comment explaining why it's disabled or enable it if it's ready for use.

r2dbc/boot-r2dbc-notify-listen/src/main/java/com/example/demo/notifier/Notifier.java (2)

23-30: LGTM! Good dependency injection and logging setup.

The addition of NotificationListener aligns with the best practice of ensuring channels are listened to before sending notifications.


65-72: LGTM! Well-structured notification logic.

The method follows good practices:

  • Uses parameterized queries for SQL injection prevention
  • Includes appropriate debug logging
  • Clean separation of concerns

Comment on lines +34 to +63
return req.bodyToMono(NotificationRequest.class)
.flatMap(notificationRequest -> {
Validator validator =
Validation.buildDefaultValidatorFactory().getValidator();
Set<ConstraintViolation<NotificationRequest>> violations = validator.validate(notificationRequest);
if (!violations.isEmpty()) {
String errorMessage = violations.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, errorMessage);
return ServerResponse.badRequest()
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
.bodyValue(problem);
}
return notificationListener
.listenTo(notificationRequest.channel())
.doOnSuccess(postgresqlResult -> log.debug(postgresqlResult.toString()))
.then(Mono.defer(() -> sendNotification(notificationRequest)
.flatMap(rowsUpdated -> ServerResponse.ok().bodyValue(rowsUpdated))
.onErrorResume(e -> {
log.error("Error sending notification: {}", e.getMessage(), e);
return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("Error sending notification: " + e.getMessage());
})));
})
.onErrorResume(error -> {
log.error("Failed to process notification request", error);
return ServerResponse.status(500).bodyValue("Failed to process notification request");
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Move validation to controller layer using annotations

The current validation implementation in the service layer goes against the application's established pattern. Consider moving the validation to the controller layer using annotations on the NotificationRequest class.

  1. Add validation annotations to NotificationRequest:
public record NotificationRequest(
    @NotBlank(message = "Channel name is required")
    @Pattern(regexp = "[a-zA-Z0-9_]+", message = "Channel name must contain only letters, numbers, and underscores")
    String channel,
    
    @NotBlank(message = "Message is required")
    String message
) {}
  1. Remove manual validation from the service layer and let Spring's validation handle it through the @Valid annotation in the controller.

@rajadilipkolli rajadilipkolli merged commit d8b1ded into main Jan 12, 2025
6 of 7 checks passed
@rajadilipkolli rajadilipkolli deleted the polish-notifier branch January 12, 2025 14:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant