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