diff --git a/backend/pom.xml b/backend/pom.xml
index 204856b..e45e086 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -82,6 +82,7 @@
org.awaitility
awaitility
+ test
diff --git a/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java b/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java
index c4e7b11..135b770 100644
--- a/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java
+++ b/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java
@@ -5,8 +5,8 @@
import com.github.binpastes.paste.api.model.SearchView;
import com.github.binpastes.paste.api.model.SearchView.SearchItemView;
import com.github.binpastes.paste.api.model.SingleView;
-import com.github.binpastes.paste.domain.Paste;
import com.github.binpastes.paste.application.PasteService;
+import com.github.binpastes.paste.domain.Paste;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
@@ -19,23 +19,13 @@
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.ExceptionHandler;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.ResponseStatus;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.time.LocalDateTime;
-import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import static com.github.binpastes.paste.api.model.ListView.ListItemView;
@@ -64,22 +54,19 @@ public Mono findPaste(@PathVariable("pasteId") String pasteId, Serve
return;
}
- if (paste.isPermanent() || isAfter(paste.getDateOfExpiry(), 5, ChronoUnit.MINUTES)) {
- response.getHeaders().setCacheControl(CacheControl.maxAge(5, TimeUnit.MINUTES));
- return;
+ var now = LocalDateTime.now();
+ if (paste.isPermanent() || paste.getDateOfExpiry().plusMinutes(5).isAfter(now)) {
+ response.getHeaders().setCacheControl(
+ CacheControl.maxAge(5, TimeUnit.MINUTES));
+ } else {
+ response.getHeaders().setCacheControl(
+ CacheControl.maxAge(Duration.between(now, paste.getDateOfExpiry())));
}
-
- response.getHeaders().setCacheControl(
- CacheControl.maxAge(Duration.between(LocalDateTime.now(), paste.getDateOfExpiry())));
})
.map(reference -> SingleView.of(reference, remoteAddress(request)))
.switchIfEmpty(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
}
- private static boolean isAfter(LocalDateTime dateTime, long amount, ChronoUnit unit) {
- return LocalDateTime.now().plus(amount, unit).isBefore(dateTime);
- }
-
@GetMapping
public Mono findPastes() {
return pasteService
diff --git a/backend/src/main/java/com/github/binpastes/paste/api/model/CreateCmd.java b/backend/src/main/java/com/github/binpastes/paste/api/model/CreateCmd.java
index e2ee385..efadad5 100644
--- a/backend/src/main/java/com/github/binpastes/paste/api/model/CreateCmd.java
+++ b/backend/src/main/java/com/github/binpastes/paste/api/model/CreateCmd.java
@@ -39,9 +39,7 @@ public String title() {
}
public String content() {
- return StringUtils.hasText(content)
- ? content
- : null;
+ return content;
}
public boolean isEncrypted() {
diff --git a/backend/src/main/java/com/github/binpastes/paste/application/PasteService.java b/backend/src/main/java/com/github/binpastes/paste/application/PasteService.java
index efec282..3fb277c 100644
--- a/backend/src/main/java/com/github/binpastes/paste/application/PasteService.java
+++ b/backend/src/main/java/com/github/binpastes/paste/application/PasteService.java
@@ -50,14 +50,15 @@ public Mono find(String id) {
}
private Mono trackAccess(Paste paste) {
+ trackingService.trackView(paste.getId());
+
if (paste.isOneTime()) {
return pasteRepository
.save(paste.markAsExpired())
- .doOnSuccess(deletedPaste -> log.info("OneTime paste {} viewed and burnt", deletedPaste.getId()))
- .onErrorComplete();
+ .retry()
+ .doOnSuccess(deletedPaste -> log.info("OneTime paste {} viewed and burnt", deletedPaste.getId()));
}
- trackingService.trackView(paste.getId());
return Mono.just(paste);
}
diff --git a/backend/src/main/java/com/github/binpastes/paste/application/tracking/TrackingService.java b/backend/src/main/java/com/github/binpastes/paste/application/tracking/TrackingService.java
index 37c8c81..4322999 100644
--- a/backend/src/main/java/com/github/binpastes/paste/application/tracking/TrackingService.java
+++ b/backend/src/main/java/com/github/binpastes/paste/application/tracking/TrackingService.java
@@ -5,9 +5,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.stereotype.Service;
-import reactor.core.publisher.Mono;
import java.time.Instant;
import java.time.LocalDateTime;
@@ -49,9 +47,8 @@ public void receiveView(String pasteId, Instant timeViewed) {
pasteRepository
.findById(pasteId)
.flatMap(paste -> pasteRepository.save(paste.trackView(timestamp)))
+ .retry()
.doOnNext(paste -> log.debug("Tracked view on paste {}", paste.getId()))
- .doOnError(OptimisticLockingFailureException.class, e -> messagingClient.sendMessage(pasteId, timeViewed))
- .onErrorResume(OptimisticLockingFailureException.class, e -> Mono.empty())
.subscribe();
}
}
diff --git a/backend/src/test/java/com/github/binpastes/paste/api/TrackingIT.java b/backend/src/test/java/com/github/binpastes/paste/api/TrackingIT.java
index 41d3be3..e889a1f 100644
--- a/backend/src/test/java/com/github/binpastes/paste/api/TrackingIT.java
+++ b/backend/src/test/java/com/github/binpastes/paste/api/TrackingIT.java
@@ -32,20 +32,17 @@
import static org.mockito.Mockito.timeout;
import static org.springframework.test.util.ReflectionTestUtils.setField;
-@SpringBootTest
+@SpringBootTest(properties = "logging.level.com.github.binpastes.paste.application.tracking=INFO")
@AutoConfigureWebTestClient
@DirtiesContext
class TrackingIT {
@Autowired
private WebTestClient webClient;
-
- @Autowired
- private PasteRepository pasteRepository;
-
@Autowired
private TrackingService trackingService;
-
+ @SpyBean
+ private PasteRepository pasteRepository;
@SpyBean
private MessagingClient messagingClient;
@@ -78,23 +75,23 @@ void trackConcurrentPasteViews() {
var intialPaste = givenPublicPaste();
Flux.fromStream(Stream.generate(intialPaste::getId))
- .take(1000)
+ .take(500)
.doOnNext(trackingService::trackView)
- // enforce a concurrent update
+ // simulate a concurrent update
.doOnNext(id -> pasteRepository.findById(id)
.doOnNext(paste -> setField(paste, "remoteAddress", "concurrentUpdate"))
.flatMap(paste -> pasteRepository.save(paste))
- .onErrorComplete() // ignore errors in test
+ .onErrorComplete()
.subscribe())
.subscribeOn(Schedulers.parallel())
.subscribe();
- await().atMost(Duration.ofMinutes(1)).pollInterval(Duration.ofMillis(500)).until(
+ await().atMost(Duration.ofMinutes(1)).pollInterval(Duration.ofSeconds(1)).until(
() -> pasteRepository.findById(intialPaste.getId()).block().getViews(),
- equalTo(1000L)
+ equalTo(500L)
);
- Mockito.verify(messagingClient, atLeast(1001)).sendMessage(any(), any());
+ Mockito.verify(pasteRepository, atLeast(500 + 100 /* contention */)).save(eq(intialPaste));
}
@Test
diff --git a/frontend/src/components/SearchPastes/SearchPastes.tsx b/frontend/src/components/SearchPastes/SearchPastes.tsx
index 404bf92..86d35b0 100644
--- a/frontend/src/components/SearchPastes/SearchPastes.tsx
+++ b/frontend/src/components/SearchPastes/SearchPastes.tsx
@@ -7,10 +7,10 @@ import styles from "./searchPastes.module.css";
type SearchPastesProps = {
term: string
pastes: Array
- onSearchEnter: (term: string) => void
+ onSearchPastes: (term: string) => void
}
-const SearchPastes: Component = ({term, pastes, onSearchEnter}): JSX.Element => {
+const SearchPastes: Component = ({term, pastes, onSearchPastes}): JSX.Element => {
let searchInput: HTMLInputElement;
@@ -18,7 +18,7 @@ const SearchPastes: Component = ({term, pastes, onSearchEnter
e.preventDefault();
if (searchInput.value?.length >= 3) {
- onSearchEnter(searchInput.value);
+ onSearchPastes(searchInput.value);
}
}
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index a364be3..bfd9b42 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -18,7 +18,7 @@ const Search: () => JSX.Element = () => {
return (searchTerm.q && searchTerm.q.length >= 3) ? searchTerm.q : null;
}
- function onSearchEnter(term: string) {
+ function onSearchPastes(term: string) {
setSearchTerm({q: term})
}
@@ -32,7 +32,7 @@ const Search: () => JSX.Element = () => {
-
+
diff --git a/pom.xml b/pom.xml
index 738e74c..9dbda66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.5
+ 3.1.6
@@ -43,7 +43,7 @@
0.8.11
- 3.1.5
+ 3.1.6
2.8.3
9.22.3
2.31.2