├── get-thread-dump-json.md ├── VTPinning.jfr ├── diag.puml ├── src ├── main │ ├── java │ │ └── victor │ │ │ └── training │ │ │ └── java │ │ │ ├── sealed │ │ │ ├── shapes │ │ │ │ ├── Shape.java │ │ │ │ ├── Square.java │ │ │ │ ├── Circle.java │ │ │ │ ├── ShapeVisitor.java │ │ │ │ └── VisitorPlay.java │ │ │ ├── some │ │ │ │ └── SomePlay.java │ │ │ └── expr │ │ │ │ └── ExprPlay.java │ │ │ ├── records │ │ │ ├── create.http │ │ │ ├── intro │ │ │ │ ├── Other.java │ │ │ │ ├── ImmutablePlay.java │ │ │ │ └── Immutable.java │ │ │ ├── RecordsIntro.java │ │ │ ├── BookRepo.java │ │ │ ├── MicroTypes.java │ │ │ └── Records.java │ │ │ ├── patterns │ │ │ ├── proxy │ │ │ │ ├── LoggedMethod.java │ │ │ │ ├── Facade.java │ │ │ │ ├── ProxyIntro.java │ │ │ │ └── LoggingAspect.java │ │ │ ├── strategy │ │ │ │ ├── HttpFilter.java │ │ │ │ └── Strategy.java │ │ │ └── template │ │ │ │ ├── Template1_Email.java │ │ │ │ └── Template2_Export.java │ │ │ ├── Util.java │ │ │ ├── varie │ │ │ ├── Maps.java │ │ │ ├── ImmutableCollections.java │ │ │ ├── SequencedCollections.java │ │ │ ├── Stream.java │ │ │ ├── FileAsString.java │ │ │ ├── OptionalApi.java │ │ │ └── InstanceOf.java │ │ │ ├── virtualthread │ │ │ ├── scopedvalues │ │ │ │ ├── ThreadLocals.java │ │ │ │ └── ScopedValues.java │ │ │ ├── util │ │ │ │ ├── Slide.java │ │ │ │ ├── DisplayVTNameInLogback.java │ │ │ │ └── RunMonitor.java │ │ │ ├── ParallelStreamWithVirtualThreads.java │ │ │ ├── AnApi.java │ │ │ ├── SpringApp.java │ │ │ ├── experiments │ │ │ │ ├── ThreadLocalCache.java │ │ │ │ ├── Deadlock.java │ │ │ │ ├── OneMillion.java │ │ │ │ └── Experiment.java │ │ │ ├── structuredconcurrency │ │ │ │ ├── structured-concurrency.md │ │ │ │ ├── Workshop.java │ │ │ │ └── WorkshopSolved.java │ │ │ └── VirtualThreadsApi.java │ │ │ └── Switch │ │ │ ├── SwitchExpression.java │ │ │ └── SwitchVoid.java │ └── resources │ │ ├── logback.xml │ │ └── application.properties └── test │ ├── resources │ ├── mappings │ │ ├── beer.json │ │ ├── vodka.json │ │ └── user-preferences.json │ └── gatling.conf │ └── java │ ├── victor │ └── training │ │ └── java │ │ ├── virtualthread │ │ ├── structuredconcurrency │ │ │ ├── WorkshopSolvedTest.java │ │ │ └── WorkshopTest.java │ │ ├── experiments │ │ │ └── ExperimentTest.java │ │ ├── BookingTest.java │ │ └── VTPinningTest.java │ │ ├── Switch │ │ └── SwitchVoidTest.java │ │ └── records │ │ └── IntegrationTest.java │ ├── DelayGatling.java │ ├── ParRestGatling.java │ ├── BeerGatling.java │ └── GatlingEngine.java ├── run-wiremock.sh ├── .gitignore ├── plan.md ├── best-features.md ├── pom.xml ├── jMeter-project.jmx └── threads.json /get-thread-dump-json.md: -------------------------------------------------------------------------------- 1 | jcmd Thread.dump_to_file -format=json threads.json -------------------------------------------------------------------------------- /VTPinning.jfr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/victorrentea/java-latest/HEAD/VTPinning.jfr -------------------------------------------------------------------------------- /diag.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | [*] --> State1 4 | State1 --> State3 5 | State1 --> State2 6 | 7 | @enduml 8 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/shapes/Shape.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.shapes; 2 | 3 | 4 | public interface Shape { 5 | } 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/shapes/Square.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.shapes; 2 | 3 | public record Square(int edge) implements Shape { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/shapes/Circle.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.shapes; 2 | 3 | 4 | public record Circle(int radius) implements Shape { 5 | } 6 | -------------------------------------------------------------------------------- /run-wiremock.sh: -------------------------------------------------------------------------------- 1 | java -jar ~/.m2/repository/com/github/tomakehurst/wiremock-standalone/2.27.2/wiremock-standalone-2.27.2.jar --port 9999 --root-dir src/test/resources --global-response-templating --async-response-enabled true -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/shapes/ShapeVisitor.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.shapes; 2 | 3 | 4 | public interface ShapeVisitor { 5 | void visit(Square square); 6 | void visit(Circle circle); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/create.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/books 2 | Content-Type: application/json 3 | 4 | { 5 | "title": "a", 6 | "authors": ["a"], 7 | "teaserVideoUrl": null 8 | } 9 | 10 | ### 11 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/proxy/LoggedMethod.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.proxy; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface LoggedMethod { 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/Util.java: -------------------------------------------------------------------------------- 1 | package victor.training.java; 2 | 3 | public class Util { 4 | public static void sleepMillis(int millis) { 5 | try { 6 | Thread.sleep(millis); 7 | } catch (InterruptedException e) { 8 | throw new RuntimeException(e); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/Maps.java: -------------------------------------------------------------------------------- 1 | import java.util.HashMap; 2 | import java.util.Map; 3 | 4 | void main() { 5 | Map map = new HashMap<>(); 6 | map.put("one", 1); 7 | map.put("two", 2); 8 | System.out.println(map); 9 | } 10 | 11 | // new HashMap<>(){{ "idiom" 12 | // Map.of 13 | // Map.ofEntries -------------------------------------------------------------------------------- /src/test/resources/mappings/beer.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/api/beer/blond" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": 9 | { 10 | "type": "blond" 11 | }, 12 | "fixedDelayMilliseconds": 1000, 13 | "headers": { 14 | "Content-Type": "application/json" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/test/resources/mappings/vodka.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/api/vodka" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": 9 | { 10 | "type": "deadly" 11 | }, 12 | "fixedDelayMilliseconds": 1000, 13 | "headers": { 14 | "Content-Type": "application/json" 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/proxy/Facade.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.proxy; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.springframework.transaction.annotation.Transactional; 5 | 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | 9 | @Service 10 | @Transactional 11 | @Retention(RetentionPolicy.RUNTIME) 12 | public @interface Facade { 13 | } -------------------------------------------------------------------------------- /src/test/resources/mappings/user-preferences.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPathPattern": "/api/user-preferences" 5 | }, 6 | "response": { 7 | "status": 200, 8 | "jsonBody": 9 | { 10 | "favoriteBeerType": "blond", 11 | "iceInVodka": "false" 12 | }, 13 | "fixedDelayMilliseconds": 1000, 14 | "headers": { 15 | "Content-Type": "application/json" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/test/java/victor/training/java/virtualthread/structuredconcurrency/WorkshopSolvedTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.structuredconcurrency; 2 | 3 | import org.junit.jupiter.api.BeforeEach; 4 | import org.mockito.InjectMocks; 5 | 6 | public class WorkshopSolvedTest extends WorkshopTest { 7 | @InjectMocks 8 | WorkshopSolved workshopSolved; 9 | 10 | @BeforeEach 11 | final void setSolved() { 12 | workshop = workshopSolved; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | .settings/* 22 | .project 23 | .classpath 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | /target/ 28 | /.idea/* 29 | *.iml 30 | /out/ 31 | *.jfr 32 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/ImmutableCollections.java: -------------------------------------------------------------------------------- 1 | import java.util.Arrays; 2 | import java.util.List; 3 | 4 | import static java.util.stream.Collectors.toList; 5 | 6 | void main() { // stand-alone 'main' v21 7 | List numbers = Arrays.asList(1, 2); 8 | numbers.set(0, -1); 9 | System.out.println(numbers); 10 | 11 | var odds = numbers.stream() 12 | .filter(i -> i % 2 == 1) 13 | .collect(toList()); 14 | odds.removeFirst(); // v21 15 | System.out.println(odds); 16 | } -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%threadId] %-5level %logger{36} -%kvp- %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/intro/Other.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records.intro; 2 | 3 | public class Other { 4 | private String data; 5 | 6 | public Other(String data) { 7 | this.data = data; 8 | } 9 | 10 | public String getData() { 11 | return data; 12 | } 13 | 14 | public Other setData(String data) { 15 | this.data = data; 16 | return this; 17 | } 18 | 19 | @Override 20 | public String toString() { 21 | return "Other{" + 22 | "data='" + data + '\'' + 23 | '}'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/victor/training/java/virtualthread/experiments/ExperimentTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.experiments; 2 | 3 | import me.escoffier.loom.loomunit.LoomUnitExtension; 4 | import me.escoffier.loom.loomunit.ShouldNotPin; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | 8 | import java.util.concurrent.Executors; 9 | 10 | @ExtendWith(LoomUnitExtension.class) 11 | public class ExperimentTest { 12 | @Test 13 | @ShouldNotPin // assert JFR Events 14 | void experiment() throws Exception { 15 | Experiment.main(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/SequencedCollections.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.varie; 2 | 3 | import java.util.*; 4 | 5 | public class SequencedCollections { 6 | public static void main(String[] args) { 7 | var list = new ArrayList<>(List.of(1, 2, 3)); 8 | var reversed = list.reversed(); 9 | System.out.println(list.getFirst() + list.removeLast()); 10 | list.addFirst(4); 11 | System.out.println(reversed.getLast()); 12 | 13 | var map = new LinkedHashMap<>(Map.of("one", 1, "two", 2)); 14 | map.putFirst("zero", 0); 15 | map.lastEntry().setValue(3); 16 | 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/Stream.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.varie; 2 | 3 | import java.io.IOException; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class Stream { 8 | 9 | 10 | public static void main(String[] args) throws IOException { 11 | List numbers = List.of(-4, -3, -2, 0, 1, 2, 3, -4, 5); 12 | 13 | System.out.println("After first negatives"); 14 | numbers.stream().dropWhile(i -> i < 0).forEach(System.out::println); 15 | 16 | System.out.println("Until the first positive"); 17 | numbers.stream().takeWhile(i -> i < 0).forEach(System.out::println); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/FileAsString.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.varie; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | 7 | public class FileAsString { 8 | public static void main(String[] args) throws IOException { 9 | File file = new File("pom.xml"); 10 | System.out.println("File path = " + file.getAbsolutePath()); 11 | System.out.println("File found = " + file.isFile()); 12 | 13 | String fullContents = Files.readString(file.toPath()); 14 | 15 | System.out.println(fullContents.length()); 16 | System.out.println(fullContents.lines().count()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /plan.md: -------------------------------------------------------------------------------- 1 | ## The Java Revolution: Best new Language Features since Java 8 2 | - 👋 Hi! I'm https://victorrentea.ro 3 | - Java Language - background and position today 4 | - var [11] 5 | - Verbosity and Project Lombok (https://projectlombok.org/) [8] 6 | - records ⭐️ [17] 7 | - immutability: List|Map.of [11], .toList [17] 8 | - text blocks """ [17] and interpolation \{var} [24] 9 | - switch statement: problems -> clean switch rules 10 | - switch expressions ⭐️ [17] 11 | - return switch(enum) [17] over polymorphism with types or enums 12 | - switch over sealed classes [21] over Visitor pattern 13 | - Virtual threads - Project Loom & friends [21] 14 | 15 | The features above are supported since Java LTS version [ver] (expected) -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/strategy/HttpFilter.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.strategy; 2 | 3 | import jakarta.servlet.*; 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.io.IOException; 8 | 9 | @Component 10 | public class HttpFilter implements Filter { 11 | @Override 12 | public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException, ServletException { 13 | HttpServletRequest request = (HttpServletRequest) req; 14 | System.out.println("Browser language: " + request.getHeader("Accept-Language")); 15 | 16 | chain.doFilter(req, res); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/shapes/VisitorPlay.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.shapes; 2 | 3 | 4 | 5 | import java.util.List; 6 | 7 | public class VisitorPlay { 8 | 9 | public static void main(String[] args) { 10 | List shapes = List.of( 11 | new Square(10), // 4 * E 12 | new Circle(5), // 2 * PI * R 13 | new Square(5), 14 | new Square(1)); 15 | 16 | double totalPerimeter = 0; // TASK : compute 17 | 18 | // ## instanceOf 19 | // ## OOP (behavior next to state) 20 | // ## VISITOR 😱 21 | // ## switch+sealed 22 | 23 | System.out.println(totalPerimeter); 24 | } 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/scopedvalues/ThreadLocals.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.scopedvalues; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | @Slf4j 6 | public class ThreadLocals { 7 | public final static ThreadLocal threadLocalUser = 8 | new ThreadLocal<>(); 9 | 10 | public static void main() { 11 | threadLocalUser.set("Victor"); 12 | log.info("Before: {}", threadLocalUser.get()); 13 | method(); 14 | log.info("After: {}", threadLocalUser.get()); 15 | } 16 | 17 | public static void method() { 18 | log.info("Same thread: {}", threadLocalUser.get()); 19 | new Thread(ThreadLocals::subtask).start(); 20 | } 21 | 22 | public static void subtask() { 23 | log.info("Child thread: {}", threadLocalUser.get()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/util/Slide.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.util; 2 | 3 | import java.util.concurrent.CompletableFuture; 4 | 5 | import static java.util.concurrent.CompletableFuture.supplyAsync; 6 | 7 | public class Slide { 8 | private static Api api; 9 | 10 | public CompletableFuture abc(String id) { 11 | return supplyAsync(() -> api.a(id)) 12 | .thenCompose(a -> api.b(a) 13 | .thenCompose(b -> api.c(a, b) 14 | .thenApply(c -> new ABC(a, b, c)))); 15 | } 16 | 17 | interface Api { 18 | String a(String id); 19 | 20 | CompletableFuture b(String a); 21 | 22 | CompletableFuture c(String a, String b); 23 | } 24 | 25 | public record ABC(String a, String b, String c) { 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.jpa.show-sql=true 2 | #spring.datasource.url = jdbc:h2:mem: 3 | #spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver 4 | #spring.datasource.username=sa 5 | #spring.datasource.password=sa 6 | logging.level.victor=DEBUG 7 | #logging.pattern.console=%clr(%d{HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:%4p}) [%15.15t] <%X{reqId}> %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n 8 | 9 | 10 | #server.tomcat.threads.max=500 11 | 12 | # tocmai ai alocat + 300 thread x 250KB de stack 13 | 14 | logging.level.org.springframework.transaction.interceptor.TransactionInterceptor=TRACE 15 | 16 | # tells tomcat to start a new Virtual Thread for every incoming HTTP request 17 | # (instead of a pool of 200 Platform Thread) 18 | spring.threads.virtual.enabled=true 19 | -------------------------------------------------------------------------------- /src/test/java/DelayGatling.java: -------------------------------------------------------------------------------- 1 | import io.gatling.javaapi.core.Simulation; 2 | 3 | import static io.gatling.javaapi.core.CoreDsl.constantConcurrentUsers; 4 | import static io.gatling.javaapi.core.CoreDsl.scenario; 5 | import static io.gatling.javaapi.http.HttpDsl.http; 6 | import static java.time.Duration.ofSeconds; 7 | 8 | public class DelayGatling extends Simulation { 9 | public static void main(String[] args) { 10 | GatlingEngine.startClass(DelayGatling.class); 11 | } 12 | 13 | { 14 | String host = "http://localhost:8080"; 15 | 16 | setUp(scenario(getClass().getSimpleName()).exec(http("") 17 | .get("/delay") 18 | ) 19 | .injectClosed(constantConcurrentUsers(300).during(ofSeconds(5)))) 20 | 21 | .protocols(http.baseUrl(host)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/ParRestGatling.java: -------------------------------------------------------------------------------- 1 | import io.gatling.javaapi.core.Simulation; 2 | 3 | import static io.gatling.javaapi.core.CoreDsl.constantConcurrentUsers; 4 | import static io.gatling.javaapi.core.CoreDsl.scenario; 5 | import static io.gatling.javaapi.http.HttpDsl.http; 6 | import static java.time.Duration.ofSeconds; 7 | 8 | public class ParRestGatling extends Simulation { 9 | public static void main(String[] args) { 10 | GatlingEngine.startClass(ParRestGatling.class); 11 | } 12 | 13 | { 14 | String host = "http://localhost:8080"; 15 | 16 | setUp(scenario(getClass().getSimpleName()).exec(http("") 17 | .get("/par") 18 | ) 19 | .injectClosed(constantConcurrentUsers(300).during(ofSeconds(5)))) 20 | 21 | .protocols(http.baseUrl(host)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/victor/training/java/Switch/SwitchVoidTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.Switch; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import victor.training.java.Switch.HRMessage.MessageType; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | class SwitchVoidTest { 9 | 10 | @Test 11 | void coversAllPossibleTypesOfMessages() { 12 | for (MessageType type : MessageType.values()) { 13 | 14 | 15 | try { 16 | new SwitchVoid().handleMessage(new HRMessage(type, "", false)); 17 | } catch (Exception e) { 18 | if (e instanceof IllegalArgumentException && !e.getMessage().equals("NU ASTA IN PROD")) { 19 | continue; 20 | } 21 | fail("entered default: "); 22 | } 23 | // TODO orice exceptie e ok, mai putin "NU ASTA IN PROD" 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/some/SomePlay.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.some; 2 | 3 | import victor.training.java.sealed.some.Maybe.Some; 4 | 5 | import java.util.Random; 6 | 7 | import static victor.training.java.sealed.some.Maybe.*; 8 | 9 | public class SomePlay { 10 | public static void main(String[] args) { 11 | switch (someRepoMethod(new Random().nextBoolean())) { 12 | case Some(var data) -> System.out.println("Got: " + data); 13 | case None() -> System.out.println("Got nada"); 14 | } 15 | } 16 | 17 | static Maybe someRepoMethod(boolean b) { 18 | return b ? new Some<>("data") : new None<>(); 19 | } 20 | } 21 | 22 | // alternative to Optional<> 23 | sealed interface Maybe{ 24 | record Some(T t) implements Maybe {} 25 | record None() implements Maybe {} 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/ParallelStreamWithVirtualThreads.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.concurrent.ExecutionException; 6 | import java.util.concurrent.ExecutorService; 7 | import java.util.concurrent.Executors; 8 | 9 | public class ParallelStreamWithVirtualThreads { 10 | public static void main(String[] args) throws ExecutionException, InterruptedException { 11 | 12 | ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); 13 | 14 | pool.submit(() -> { 15 | List numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); 16 | numbers.parallelStream() 17 | 18 | .forEach(x -> { 19 | System.out.println("Thread: " + Thread.currentThread() + " " + x); 20 | }); 21 | }).get(); 22 | 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/OptionalApi.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.varie; 2 | 3 | import victor.training.java.Util; 4 | 5 | import java.util.Optional; 6 | 7 | public class OptionalApi { 8 | 9 | public static void main(String[] args) { 10 | new OptionalApi().fetchFromDBorRemote(1); 11 | } 12 | 13 | // TODO this code has bad performance. Why? 14 | public String fetchFromDBorRemote(int id) { 15 | Optional dbOpt = repoFindById(id); 16 | 17 | Foo resolved = dbOpt.orElse(networkCallToRemoteSystem(id)); 18 | 19 | return resolved.value(); 20 | } 21 | 22 | private static Foo networkCallToRemoteSystem(int id) { 23 | Util.sleepMillis(3000); 24 | return new Foo("From API id=" + id); 25 | } 26 | 27 | private static Optional repoFindById(int id) { 28 | return Optional.of(new Foo("From DB id=" + id)); 29 | } 30 | 31 | record Foo(String value) {} 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/intro/ImmutablePlay.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records.intro; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.stream.IntStream; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class ImmutablePlay { 12 | 13 | @Test 14 | void immutables() { 15 | List numbers = new ArrayList<>(IntStream.range(1, 10).boxed().toList()); 16 | Immutable obj = new Immutable("John", 17 | new Other("halo"), 18 | numbers); 19 | 20 | String original = obj.toString(); 21 | System.out.println("Initial: " + obj); 22 | 23 | unknownFierceCode(obj); 24 | 25 | System.out.println("After: " + obj); 26 | 27 | assertThat(original).describedAs("State should not change!").isEqualTo(obj.toString()); 28 | } 29 | 30 | private static void unknownFierceCode(Immutable obj) { 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/test/java/BeerGatling.java: -------------------------------------------------------------------------------- 1 | import io.gatling.javaapi.core.Simulation; 2 | 3 | import static io.gatling.javaapi.core.CoreDsl.constantConcurrentUsers; 4 | import static io.gatling.javaapi.core.CoreDsl.scenario; 5 | import static io.gatling.javaapi.http.HttpDsl.http; 6 | import static java.time.Duration.ofSeconds; 7 | 8 | public class BeerGatling extends Simulation { 9 | public static void main(String[] args) { 10 | GatlingEngine.startClass(BeerGatling.class); 11 | } 12 | 13 | { 14 | String host = "http://localhost:8080"; 15 | 16 | setUp(scenario(getClass().getSimpleName()).exec(http("") 17 | .get("/beer") 18 | ) 19 | .injectClosed(constantConcurrentUsers(500).during(ofSeconds(5)))) 20 | // poti observa aici un thread starvation issue: toate cele 21 | // 500 gramada care pot folosi doar 200 de threaduri cat are Tomcat default 22 | 23 | .protocols(http.baseUrl(host)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/sealed/expr/ExprPlay.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.sealed.expr; 2 | 3 | 4 | import static victor.training.java.sealed.expr.Expr.*; 5 | 6 | public class ExprPlay { 7 | public static void main(String[] args) { 8 | // = "1 + 2" 9 | Expr expr1 = null; // TODO new Sum(new Const(1), new Const(2)); 10 | System.out.println(print(expr1) + " => " + eval(expr1)); 11 | 12 | // = "-5 + 2" 13 | Expr expr2 = null; // TODO 14 | System.out.println(print(expr2) + " => " + eval(expr2)); 15 | 16 | // = "-5 + 2 * 7" 17 | Expr expr3 = null; // TODO 18 | System.out.println(print(expr3) + " => " + eval(expr3)); 19 | } 20 | 21 | static int eval(Expr expr) { 22 | throw new RuntimeException("Not implemented"); // TODO implement 23 | } 24 | static String print(Expr expr) { 25 | return "TODO"; // TODO 26 | } 27 | } 28 | // TODO Model expressions using sealed classes, for example: 29 | // sealed interface Expr {record Const(int c) implements Expr {} ...} 30 | interface Expr {} 31 | 32 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/varie/InstanceOf.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.varie; 2 | 3 | public class InstanceOf { 4 | public static void main(String[] args) { 5 | Position position = new Position(1, 2, 3); 6 | Color color = new Color(255, 255, 255, 1); 7 | Point point = new Point(position, color); 8 | 9 | method(point); 10 | } 11 | 12 | record Color(int r, int g, int b, float a) { 13 | } 14 | record Point(Position position, Color color) { 15 | } 16 | record Position(int x,int y, int z) { 17 | } 18 | 19 | private static void method(Object obj) { 20 | // if (obj instanceof Point(Position ...)) { 21 | if (obj instanceof Point point) { 22 | Position position = point.position(); 23 | System.out.println("At position = " + position); 24 | 25 | Color color = point.color(); 26 | System.out.println("⍺=" + color.a()); 27 | System.out.println("R=" + color.r()); 28 | System.out.println("G=" + color.g()); 29 | System.out.println("B=" + color.b()); 30 | } 31 | } 32 | } 33 | 34 | -------------------------------------------------------------------------------- /src/test/java/victor/training/java/virtualthread/BookingTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import org.junit.jupiter.api.BeforeAll; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | 8 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 9 | 10 | @SpringBootTest 11 | 12 | public class BookingTest { 13 | 14 | private static WireMockServer wireMock = new WireMockServer(9999); 15 | 16 | @BeforeAll 17 | public static void startWireMock() { 18 | wireMock.start(); 19 | } 20 | 21 | @Test 22 | void explore() { 23 | wireMock.stubFor(get(urlEqualTo("/booking-provider-1")).willReturn( 24 | aResponse().withFixedDelay(10).withBody("Booking1"))); 25 | wireMock.stubFor(get(urlEqualTo("/booking-provider-2")).willReturn( 26 | aResponse().withFixedDelay(10).withBody("Booking2"))); 27 | wireMock.stubFor(get(urlEqualTo("/weather")).willReturn( 28 | aResponse().withFixedDelay(10).withBody("Rain"))); 29 | 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/AnApi.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RestController; 5 | 6 | import java.util.concurrent.CompletableFuture; 7 | import java.util.concurrent.Executors; 8 | import java.util.concurrent.ScheduledExecutorService; 9 | import java.util.concurrent.atomic.AtomicInteger; 10 | 11 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 12 | 13 | @RestController 14 | public class AnApi { 15 | private static final AtomicInteger counter = new AtomicInteger(0); 16 | private static final ScheduledExecutorService scheduler = 17 | Executors.newScheduledThreadPool(10); 18 | 19 | @GetMapping("call") 20 | // @RunOnVirtualThread // can be implemented 21 | public CompletableFuture call() { 22 | 23 | 24 | 25 | 26 | 27 | CompletableFuture cf = new CompletableFuture<>(); 28 | scheduler.schedule( 29 | () -> cf.complete("data"+ counter.incrementAndGet()), 30 | 5000, 31 | MILLISECONDS); 32 | return cf; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/scopedvalues/ScopedValues.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.scopedvalues; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.util.concurrent.StructuredTaskScope.ShutdownOnSuccess; 7 | 8 | import static java.util.concurrent.Executors.callable; 9 | 10 | @Slf4j 11 | public class ScopedValues { 12 | public final static ScopedValue scopedUser = ScopedValue.newInstance(); 13 | 14 | public static void main() { 15 | ScopedValue.where(scopedUser, "Victor").run(() -> { 16 | log.info("Before: {}", scopedUser.get()); 17 | method(); 18 | log.info("After: {}", scopedUser.get()); 19 | }); 20 | } 21 | 22 | @SneakyThrows 23 | public static void method() { 24 | log.info("Same thread: {}", scopedUser.get()); 25 | try (var scope = new ShutdownOnSuccess<>()) { 26 | scope.fork(callable(ScopedValues::subtask)); 27 | scope.fork(callable(ScopedValues::subtask)); 28 | scope.join(); 29 | } 30 | } 31 | public static void subtask() { 32 | log.info("Child thread ({}): {}",Thread.currentThread(), scopedUser.get()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/RecordsIntro.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records; 2 | 3 | import java.util.Objects; 4 | 5 | public class RecordsIntro { 6 | public static void main(String[] args) { 7 | Point point = new Point(); 8 | point.setX(1); 9 | point.setY(2); 10 | System.out.println(point); 11 | } 12 | } 13 | 14 | class Point { 15 | private int x; 16 | private int y; 17 | 18 | // traditional Java boilerplate 🤢🤢🤢🤢 19 | public int getX() { 20 | return x; 21 | } 22 | 23 | public void setX(int x) { 24 | this.x = x; 25 | } 26 | 27 | public int getY() { 28 | return y; 29 | } 30 | 31 | public void setY(int y) { 32 | this.y = y; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "Point{x=" + x + ", y=" + y + '}'; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(x, y); 43 | } 44 | 45 | @Override 46 | public boolean equals(Object o) { 47 | if (this == o) return true; 48 | if (o == null || getClass() != o.getClass()) return false; 49 | Point point = (Point) o; 50 | return x == point.x && y == point.y; 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/SpringApp.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.web.embedded.tomcat.TomcatProtocolHandlerCustomizer; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import java.util.concurrent.Executor; 12 | import java.util.concurrent.Executors; 13 | 14 | @Slf4j 15 | @SpringBootApplication 16 | @RestController 17 | public class SpringApp { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(SpringApp.class, args); 21 | } 22 | 23 | @Bean 24 | public RestTemplate rest() { 25 | return new RestTemplate(); 26 | } 27 | 28 | @Bean 29 | public Executor virtualExecutor() { 30 | // TODO vrentea 12.09.2024: un executor spring wrapuind VT executor + lift de thread locals 31 | // TODO vrentea 12.09.2024: use java 22/23 + lombok upgrade 32 | return Executors.newVirtualThreadPerTaskExecutor(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /best-features.md: -------------------------------------------------------------------------------- 1 | ## Records 17 2 | - RecordsIntro.java 3 | - ✅ Value Objects, 🪦 @Value (Lombok) 4 | - Records.java 5 | - ✅ DTOs 6 | - Optional<> field! 7 | - ❌ @Entity, but ✅ @Embeddable (JPA) 8 | - ✅ @Document (Mongo) 9 | - ❌ @Service & co (Spring) 10 | - MicroTypes.java 11 | - ✅ vs Tuple (RX) 12 | 13 | ## Immutable Collections 11,17 14 | - ImmutableCollections.java 15 | 16 | ## Text Blocks 17,25 17 | - BookRepo.java: ✅ @Query 18 | - IntegrationTest.java: ✅ JSON 19 | + "%s".formatted(a) 20 | + STR."\{a}" 💖 21 | 22 | ## Switch (enum) idiom 17 23 | - switch(enum) 24 | - string -> enum 25 | - BUG! 0 26 | - polymorphism 27 | - enum + abstract 28 | - enum + Function 29 | - return switch 30 | Ideal: variable calculations per a type/code 31 | 32 | ## Switch Pattern Matching 21 33 | switch(sealed classes) 34 | - shapes 35 | - expr 36 | Ideal: behavior operating on a hierarchy of objects that can't go IN that hierarchy (OOP). 37 | 38 | ## Virtual Threads 21 39 | - WebFlux .fetchUser.map(getprefs).ifEmptyDefault(...).flatMap(getBeer()).doOnNext 40 | - CompletableFuture1+2.combine => scope => cf + newVirtualThreads 41 | Ideal: systems serving many requests/second and/or talking to slow systems. 42 | ! synchronized 43 | ! CPU-bound flows -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/intro/Immutable.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records.intro; 2 | 3 | import java.util.List; 4 | import java.util.Objects; 5 | 6 | public class Immutable { 7 | private final String name; 8 | private final Other other; 9 | private final List list; 10 | 11 | public Immutable(String name, Other other, List list) { 12 | this.name = name; 13 | this.other = other; 14 | this.list = list; 15 | } 16 | 17 | public String getName() { 18 | return name; 19 | } 20 | 21 | public Other getOther() { 22 | return other; 23 | } 24 | 25 | public List getList() { 26 | return list; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "Immutable{" +"name='" + name + '\'' +", other=" + other +", list=" + list +'}'; 32 | } 33 | 34 | @Override 35 | public boolean equals(Object o) { 36 | if (this == o) return true; 37 | if (o == null || getClass() != o.getClass()) return false; 38 | Immutable immutable = (Immutable) o; 39 | return Objects.equals(name, immutable.name) && Objects.equals(other, immutable.other) && Objects.equals(list, immutable.list); 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return Objects.hash(name, other, list); 45 | } 46 | } -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/BookRepo.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.data.jpa.repository.Query; 5 | import victor.training.java.records.BookApi.SearchBookResult; 6 | 7 | import java.util.List; 8 | 9 | public interface BookRepo extends JpaRepository { 10 | @Query("select new BookApi$SearchBookResult(book.id, book.title)\n" + 11 | "from Book book\n" + 12 | "where UPPER(book.name) LIKE UPPER('%' || ?1 || '%')") 13 | List search(String name); 14 | 15 | //region complex native SQL 16 | @Query(nativeQuery = true, value = 17 | "select t.id\n" + 18 | "from TEACHER t\n" + 19 | "where (?1 is null or upper(t.name) like upper(('%'||?1||'%')))\n" + 20 | "and (?2 is null or t.grade=?2)\n" + 21 | "and (cast(?3 as integer)=0 or exists\n" + 22 | " select 1\n" + 23 | " from TEACHING_ACTIVITY ta\n" + 24 | " inner join TEACHING_ACTIVITY_TEACHER tat on ta.id=tat.activities_id\n" + 25 | " inner join TEACHER tt on tat.teachers_id=tt.id\n" + 26 | " where ta.discr='COURSE'\n" + 27 | " and tt.id=t.id))\n") 28 | List complexQuery(String namePart, Integer grade, boolean teachingCourses); 29 | //endregion 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/util/DisplayVTNameInLogback.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.util; 2 | 3 | import ch.qos.logback.classic.pattern.ClassicConverter; 4 | import ch.qos.logback.classic.spi.ILoggingEvent; 5 | 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | public class DisplayVTNameInLogback extends ClassicConverter { 10 | // example: VirtualThread[#23]/runnable@ForkJoinPool-1-worker-2 11 | private static Pattern virtualThreadNamePattern = Pattern.compile("VirtualThread\\[#(\\d+)[^]]+]/runnable@ForkJoinPool-(\\d+)-worker-(\\d+)"); 12 | 13 | @Override 14 | public String convert(final ILoggingEvent e) { 15 | Thread thread = Thread.currentThread(); 16 | if (thread.isVirtual()) { 17 | return friendlyVTName(thread.toString()); 18 | } 19 | return String.valueOf(thread.toString()); 20 | } 21 | 22 | public static String friendlyVTName(String threadToString) { 23 | Matcher matcher = virtualThreadNamePattern.matcher(threadToString); 24 | if (!matcher.matches()) { 25 | System.err.println( 26 | "Virtual thread name '" + threadToString + "' does not match the expected pattern: '" + virtualThreadNamePattern.pattern() + 27 | "'. Please update the pattern in " + DisplayVTNameInLogback.class.getName()); 28 | } 29 | return "VT#" + matcher.group(1) + "/PT#" + matcher.group(3); 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/experiments/ThreadLocalCache.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.experiments; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import jakarta.xml.bind.Marshaller; 5 | import org.springframework.web.client.RestTemplate; 6 | import victor.training.java.Util; 7 | 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | 11 | public class ThreadLocalCache { 12 | public static void main(String[] args) { 13 | try (ExecutorService executor = Executors.newFixedThreadPool(10)) { 14 | // try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { // #2 VT->hijacks thread reuse 15 | for (int i = 0; i < 10000; i++) { 16 | executor.submit(() -> { 17 | someLibCode(); 18 | Util.sleepMillis(10); // pretend some ops 19 | someLibCode(); 20 | }); 21 | } 22 | } 23 | } 24 | 25 | // --- inside a legacy library --- 26 | private static final ThreadLocal threadCache = ThreadLocal.withInitial(ThreadLocalCache::expensiveInitOp); 27 | 28 | private static String expensiveInitOp() { 29 | return "library cached data " + "x".repeat(10000); 30 | } 31 | 32 | public static void someLibCode() { 33 | String data = expensiveInitOp(); 34 | // String data = threadCache.get(); // #1 cache on thread 35 | System.out.println(data.substring(0, 20)); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/strategy/Strategy.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.strategy; 2 | 3 | import lombok.Data; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.time.LocalDate; 7 | 8 | 9 | class Strategy { 10 | public static void main(String[] args) { 11 | CustomsService service = new CustomsService(); 12 | Parcel ro = new Parcel("RO", 100, 100, LocalDate.now()); 13 | System.out.println("Tax for " + ro + " = " + service.calculateCustomsTax(ro)); 14 | Parcel cn = new Parcel("CN", 100, 100, LocalDate.now()); 15 | System.out.println("Tax for " + cn + " = " + service.calculateCustomsTax(cn)); 16 | Parcel uk = new Parcel("UK", 100, 100, LocalDate.now()); 17 | System.out.println("Tax for " + uk + " = " + service.calculateCustomsTax(uk)); 18 | } 19 | } 20 | 21 | record Parcel( 22 | String originCountry, 23 | double tobaccoValue, 24 | double regularValue, 25 | LocalDate date) { 26 | } 27 | 28 | @Service 29 | @Data 30 | class CustomsService { 31 | public double calculateCustomsTax(Parcel parcel) { 32 | switch (parcel.originCountry()) { 33 | case "UK": 34 | return parcel.tobaccoValue() / 2 + parcel.regularValue(); 35 | case "CN": 36 | return parcel.tobaccoValue() + parcel.regularValue(); 37 | case "FR": 38 | case "ES": 39 | case "RO": 40 | return parcel.tobaccoValue() / 3; 41 | default: 42 | throw new IllegalArgumentException("Not a valid country ISO2 code: " + parcel.originCountry()); 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/Switch/SwitchExpression.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.Switch; 2 | 3 | 4 | import java.time.LocalDate; 5 | import java.util.List; 6 | import java.util.stream.Stream; 7 | 8 | import static java.lang.Double.parseDouble; 9 | 10 | class SwitchExpression { 11 | public static void main(String[] args) { 12 | Stream.of( 13 | "RO|100|100|2021-01-01", 14 | "CN|100|100|2021-01-01", 15 | "UK|100|100|2021-01-01" 16 | ).forEach(SwitchExpression::process); 17 | } 18 | 19 | // parsing (infrastructure) 20 | private static void process(String flatParcelLine) { 21 | String[] a = flatParcelLine.split("\\|"); 22 | Parcel parcel = new Parcel(a[0], parseDouble(a[1]), parseDouble(a[2]), LocalDate.parse(a[3])); 23 | System.out.println(calculateTax(parcel)); 24 | } 25 | 26 | // core domain logic 27 | public static double calculateTax(Parcel parcel) { 28 | double result = 0; 29 | switch (parcel.originCountry()) { 30 | case "UK": 31 | result = parcel.tobaccoValue() / 2 + parcel.regularValue(); 32 | break; 33 | case "CN": 34 | result = parcel.tobaccoValue() + parcel.regularValue(); 35 | break; 36 | case "RO": 37 | result = parcel.tobaccoValue() / 3; 38 | break; 39 | } 40 | return result; 41 | } 42 | } 43 | 44 | record Parcel( 45 | String originCountry, 46 | double tobaccoValue, 47 | double regularValue, 48 | LocalDate date) { 49 | } 50 | 51 | enum CountryEnum { 52 | RO, 53 | UK, 54 | CN, 55 | } 56 | 57 | // explore: non-enhaustive vs default? 58 | // case null: (java 21) -------------------------------------------------------------------------------- /src/test/java/victor/training/java/records/IntegrationTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records; 2 | 3 | import com.github.tomakehurst.wiremock.WireMockServer; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; 9 | import org.springframework.test.web.servlet.MockMvc; 10 | 11 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 12 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 13 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 14 | 15 | @SpringBootTest 16 | @AutoConfigureMockMvc 17 | @AutoConfigureWireMock(port = 0) // random 18 | public class IntegrationTest { 19 | @Autowired 20 | MockMvc mockMvc; 21 | 22 | @Test 23 | void apiTest() throws Exception { 24 | mockMvc.perform(post("/books") 25 | .contentType("application/json") 26 | .content(""" 27 | { 28 | "title":"name", 29 | "authors":["author1"], 30 | "teaserVideoUrl": null 31 | } 32 | """) 33 | ) 34 | .andExpect(status().isOk()); // 200 OK 35 | } 36 | 37 | 38 | @Autowired 39 | WireMockServer wireMock; 40 | 41 | @Test 42 | void wireMockStubbing() { 43 | wireMock.stubFor(get(urlMatching("/emails")) 44 | .willReturn(okJson(""" 45 | { 46 | "emails":["a@b.com"] 47 | }"""))); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/Switch/SwitchVoid.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.Switch; 2 | 3 | 4 | import org.springframework.context.event.EventListener; 5 | 6 | import java.time.LocalDate; 7 | 8 | import static victor.training.java.Switch.CountryEnum.*; 9 | 10 | 11 | class SwitchVoid { 12 | public void handleMessage(HRMessage message) { 13 | switch (message.type()) { 14 | case RAISE: 15 | handleRaiseSalary(message.content()); 16 | break; 17 | case PROMOTE: 18 | handlePromote(message.content()); 19 | break; 20 | case DISMISS: 21 | if (message.urgent()) 22 | handleDismissUrgent(message.content()); 23 | else 24 | handleDismiss(message.content()); 25 | break; 26 | default: 27 | throw new IllegalArgumentException("Should never happen in prod: unknown message type" + message.type()); 28 | } 29 | } 30 | 31 | private Void handlePromote(String content) { 32 | return null; 33 | } 34 | 35 | private Void handleDismissUrgent(String content) { 36 | System.out.println(":( !!"); 37 | return null; 38 | } 39 | private Void handleDismiss(String content) { 40 | System.out.println(":( !!"); 41 | return null; 42 | } 43 | 44 | private Void handleRaiseSalary(String content) { 45 | System.out.println(":)"); 46 | return null; 47 | } 48 | 49 | } 50 | 51 | 52 | 53 | record HRMessage(MessageType type, String content, boolean urgent) { 54 | enum MessageType { 55 | DISMISS, RAISE, PROMOTE, ANGAJARE 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/MicroTypes.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records; 2 | 3 | import org.jooq.lambda.tuple.Tuple; 4 | import org.jooq.lambda.tuple.Tuple2; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.core.ParameterizedTypeReference; 7 | import org.springframework.http.HttpMethod; 8 | import org.springframework.http.RequestEntity; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.client.RestTemplate; 11 | 12 | import java.net.URI; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Set; 16 | 17 | import static java.util.stream.Collectors.joining; 18 | import static org.assertj.core.api.Assertions.assertThat; 19 | 20 | public class MicroTypes { 21 | 22 | public Map>> extremeFP() { 23 | Long customerId = 1L; 24 | Integer product1Count = 2; 25 | Integer product2Count = 4; 26 | return Map.of(customerId, Set.of( 27 | Tuple.tuple("Table", product1Count), 28 | Tuple.tuple("Chair", product2Count) 29 | )); 30 | } 31 | 32 | void lackOfAbstractions() { 33 | Map>> map = extremeFP(); 34 | // 🚫Don't use 'var' above 35 | 36 | for (Long cid : map.keySet()) { 37 | String pl = map.get(cid).stream() 38 | .map(t -> t.v2 + " of " + t.v1) 39 | .collect(joining(", ")); 40 | System.out.println("cid=" + cid + " got " + pl); 41 | } 42 | } 43 | 44 | public void varUsecase() { 45 | ResponseEntity> response = new RestTemplate().exchange(new RequestEntity<>(HttpMethod.POST, URI.create("http://some-url")), new ParameterizedTypeReference>() { 46 | }); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/template/Template1_Email.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.template; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.Random; 6 | 7 | public class Template1_Email { 8 | public static void main(String[] args) { 9 | placeOrder(); 10 | shipOrder(); 11 | } 12 | 13 | private static void placeOrder() { 14 | // logic 15 | new EmailService().sendOrderPlacedEmail("a@b.com"); 16 | } 17 | 18 | private static void shipOrder() { 19 | // logic 20 | // TODO implement 'similar to how order placed email was implemented' 21 | } 22 | } 23 | 24 | class EmailService { 25 | public void sendOrderPlacedEmail(String emailAddress) { 26 | EmailContext context = new EmailContext(/*smtpConfig,etc*/); 27 | int MAX_RETRIES = 3; 28 | try { 29 | for (int i = 0; i < MAX_RETRIES; i++) { 30 | Email email = new Email(); // constructor generates new unique ID 31 | email.setSender("noreply@corp.com"); 32 | email.setReplyTo("/dev/null"); 33 | email.setTo(emailAddress); 34 | email.setSubject("Order Placed"); 35 | email.setBody("Thank you for your order"); 36 | boolean success = context.send(email); 37 | if (success) break; 38 | } 39 | } catch (Exception e) { 40 | throw new RuntimeException("Can't send email", e); 41 | } 42 | } 43 | } 44 | 45 | class EmailContext { 46 | public boolean send(Email email) { 47 | System.out.println("Trying to send " + email); 48 | return new Random(System.nanoTime()).nextBoolean(); 49 | } 50 | } 51 | 52 | @Data 53 | class Email { 54 | private final long id = new Random(System.nanoTime()).nextLong(); 55 | private String subject; 56 | private String body; 57 | private String sender; 58 | private String replyTo; 59 | private String to; 60 | } -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/experiments/Deadlock.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.experiments; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import static java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor; 7 | 8 | @Slf4j 9 | public class Deadlock { 10 | // inspired by https://blog.ydb.tech/how-we-switched-to-java-21-virtual-threads-and-got-deadlock-in-tpc-c-for-postgresql-cca2fe08d70b 11 | // Also see deadlock-vthread-dump.txt 12 | public static void main() throws InterruptedException { 13 | try (var virtual = newVirtualThreadPerTaskExecutor()) { 14 | for (int i = 0; i < 1000; i++) { 15 | virtual.submit(() -> work()); 16 | } 17 | } 18 | } 19 | 20 | private static final Object lock = new Object(); 21 | static int availableConnections = 4; 22 | 23 | @SneakyThrows 24 | public static void work() { 25 | var connection = acquireConnection(); 26 | 27 | log.info("using the resource"); 28 | writeOnConnection(connection); 29 | 30 | releaseConnection(connection); 31 | log.info("done"); 32 | } 33 | 34 | private static String acquireConnection() throws InterruptedException { 35 | log.info("acquiring..."); 36 | synchronized (lock) { 37 | while (availableConnections == 0) { 38 | lock.wait(); 39 | } 40 | availableConnections--; 41 | log.info("acquired connection. left = " + availableConnections); 42 | } 43 | return "connection"; 44 | } 45 | 46 | private static void releaseConnection(String connection) { 47 | log.info("releasing..."); 48 | synchronized (lock) { 49 | availableConnections++; 50 | lock.notifyAll(); 51 | } 52 | } 53 | 54 | @SneakyThrows 55 | private static void writeOnConnection(String connection) { 56 | synchronized (connection) { 57 | log.info("writing..."); 58 | Thread.sleep(10); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/structuredconcurrency/structured-concurrency.md: -------------------------------------------------------------------------------- 1 | For example here is a method, handle(), that represents a task in a server application. It handles an incoming request by submitting two subtasks to an ExecutorService. One subtask executes the method findUser() and the other subtask executes the method fetchOrder(). The ExecutorService immediately returns a Future for each subtask, and executes each subtask in its own thread. The handle() method awaits the subtasks' results via blocking calls to their futures' get() methods, so the task is said to join its subtasks. 2 | 3 | ```java 4 | Response handle() throws ExecutionException, InterruptedException { 5 | Future user = esvc.submit(() -> findUser()); 6 | Future order = esvc.submit(() -> fetchOrder()); 7 | String theUser = user.get(); // Join findUser 8 | int theOrder = order.get(); // Join fetchOrder 9 | return new Response(theUser, theOrder); 10 | } 11 | ``` 12 | 13 | Because the subtasks execute concurrently, each subtask can succeed or fail independently. (Failure, in this context, means to throw an exception.) Often, a task such as handle() should fail if any of its subtasks fail. Understanding the lifetimes of the threads can be surprisingly complicated when failure occurs: 14 | 15 | - If findUser() throws an exception then handle() will throw an exception when calling user.get() but fetchOrder() will continue to run in its own thread. This is a thread leak which, at best, wastes resources; at worst, the fetchOrder() thread will interfere with other tasks. 16 | 17 | - If the thread executing handle() is interrupted, the interruption will not propagate to the subtasks. Both the findUser() and fetchOrder() threads will leak, continuing to run even after handle() has failed. 18 | 19 | - If findUser() takes a long time to execute, but fetchOrder() fails in the meantime, then handle() will wait unnecessarily for findUser() by blocking on user.get() rather than cancelling it. Only after findUser() completes and user.get() returns will order.get() throw an exception, causing handle() to fail. -------------------------------------------------------------------------------- /src/main/java/victor/training/java/records/Records.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.records; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.Id; 6 | import jakarta.validation.constraints.NotBlank; 7 | import jakarta.validation.constraints.NotEmpty; 8 | import lombok.Data; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.boot.SpringApplication; 11 | import org.springframework.boot.autoconfigure.SpringBootApplication; 12 | import org.springframework.transaction.annotation.Transactional; 13 | import org.springframework.validation.annotation.Validated; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.List; 17 | 18 | @SpringBootApplication 19 | public class Records { 20 | public static void main(String[] args) { 21 | SpringApplication.run(Records.class, args); 22 | } 23 | } 24 | 25 | @RestController 26 | @RequiredArgsConstructor 27 | //record BookApi(BookRepo bookRepo) { // 🛑DON'T: proxies don't work on final classes => eg @Secured/@Transactional.. won't work 28 | class BookApi { 29 | private final BookRepo bookRepo; 30 | 31 | // DTO 32 | public record CreateBookRequest( 33 | @NotBlank String title, 34 | @NotEmpty List authors, 35 | String teaserVideoUrl // may be absent 36 | ) { 37 | } 38 | 39 | @PostMapping("books") 40 | @Transactional 41 | public void createBook(@RequestBody @Validated CreateBookRequest request) { 42 | System.out.println("pretend save title:" + request.title() + " and url:" + request.teaserVideoUrl()); 43 | System.out.println("pretend save authors: " + request.authors()); 44 | } 45 | 46 | // ---- 47 | 48 | public record SearchBookResult( 49 | long id, 50 | String name 51 | ) { 52 | } 53 | 54 | @GetMapping("books") 55 | public List search(@RequestParam String name) { 56 | return bookRepo.search(name); 57 | } 58 | 59 | } 60 | 61 | @Entity 62 | @Data // avoid @Data on @Entity 63 | class Book { 64 | @Id 65 | @GeneratedValue 66 | private Long id; 67 | private String title; 68 | 69 | private String authorFirstName; 70 | private String authorLastName; 71 | } 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/proxy/ProxyIntro.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.proxy; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.stereotype.Service; 7 | 8 | @SpringBootApplication 9 | public class ProxyIntro { 10 | public static void main(String[] args) { 11 | // Play the role of Spring here (there's no framework) 12 | // TODO 1 : LOG the arguments of any invocation of a method in Maths w/ decorator 13 | // TODO 2 : without changing anything below the line (w/o any interface) 14 | // TODO 3 : so that any new methods in Maths are automatically logged [hard] 15 | 16 | Maths maths = new Maths(); 17 | 18 | SecondGrade secondGrade = new SecondGrade(maths); 19 | 20 | new ProxyIntro().run(secondGrade); 21 | 22 | // TODO 4 : let Spring do its job, and do the same with an Aspect 23 | SpringApplication.run(ProxyIntro.class, args); 24 | } 25 | 26 | // =============== THE LINE ================= 27 | 28 | @Autowired 29 | public void run(SecondGrade secondGrade) { 30 | System.out.println("At runtime..."); 31 | secondGrade.mathClass(); 32 | } 33 | 34 | } 35 | 36 | @Service 37 | class SecondGrade { 38 | private final Maths maths; 39 | 40 | SecondGrade(Maths maths) { 41 | this.maths = maths; 42 | } 43 | 44 | public void mathClass() { 45 | System.out.println("2+4=" + maths.sum(2, 4)); 46 | System.out.println("1+5=" + maths.sum(1, 5)); 47 | System.out.println("2x3=" + maths.product(2, 3)); 48 | } 49 | } 50 | 51 | @Service 52 | class Maths { 53 | public int sum(int a, int b) { 54 | return a + b; 55 | } 56 | 57 | public int product(int a, int b) { 58 | int total = 0; 59 | for (int i = 0; i < a; i++) { 60 | total = sum(total, b); 61 | } 62 | return total; 63 | } 64 | } 65 | 66 | 67 | // Key Points 68 | // [2] Class Proxy using CGLIB (Enhancer) extending the proxied class 69 | // [3] Spring Cache support [opt: redis] 70 | // [4] Custom @Aspect, applied to methods in @Facade 71 | // [6] Tips: self proxy, debugging, final 72 | // [7] OPT: Manual proxying using BeanPostProcessor 73 | -------------------------------------------------------------------------------- /src/test/java/GatlingEngine.java: -------------------------------------------------------------------------------- 1 | import io.gatling.app.Gatling; 2 | import io.gatling.core.config.GatlingPropertiesBuilder; 3 | 4 | import java.net.URISyntaxException; 5 | import java.nio.file.Path; 6 | import java.nio.file.Paths; 7 | 8 | public class GatlingEngine { 9 | public static void main(String[] args) { 10 | GatlingPropertiesBuilder props = new GatlingPropertiesBuilder() 11 | .resourcesDirectory(mavenResourcesDirectory().toString()) 12 | .resultsDirectory(resultsDirectory().toString()) 13 | .binariesDirectory(mavenBinariesDirectory().toString()); 14 | Gatling.fromMap(props.build()); 15 | 16 | } 17 | 18 | protected static void startClass(Class clazz) { 19 | GatlingPropertiesBuilder props = new GatlingPropertiesBuilder() 20 | .resourcesDirectory(mavenResourcesDirectory().toString()) 21 | .resultsDirectory(resultsDirectory().toString()) 22 | .binariesDirectory(mavenBinariesDirectory().toString()) 23 | .simulationClass(clazz.getCanonicalName()); 24 | Gatling.fromMap(props.build()); 25 | } 26 | 27 | 28 | public static Path projectRootDir() { 29 | try { 30 | return Paths.get(GatlingEngine.class.getClassLoader().getResource("gatling.conf").toURI()) 31 | .getParent().getParent().getParent(); 32 | } catch (URISyntaxException e) { 33 | throw new RuntimeException(e); 34 | } 35 | } 36 | 37 | public static Path mavenTargetDirectory() { 38 | return projectRootDir().resolve("target"); 39 | } 40 | 41 | public static Path mavenSrcTestDirectory() { 42 | return projectRootDir() .resolve("src").resolve("test"); 43 | } 44 | 45 | 46 | public static Path mavenSourcesDirectory() { 47 | return mavenSrcTestDirectory().resolve("java"); 48 | } 49 | 50 | public static Path mavenResourcesDirectory() { 51 | return mavenSrcTestDirectory().resolve("resources"); 52 | } 53 | 54 | public static Path mavenBinariesDirectory() { 55 | return mavenTargetDirectory().resolve("test-classes"); 56 | } 57 | 58 | public static Path resultsDirectory() { 59 | return mavenTargetDirectory().resolve("gatling"); 60 | } 61 | 62 | public static Path recorderConfigFile() { 63 | return mavenResourcesDirectory().resolve("recorder.conf"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/experiments/OneMillion.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.experiments; 2 | 3 | import com.sun.management.OperatingSystemMXBean; 4 | import lombok.SneakyThrows; 5 | 6 | import java.io.IOException; 7 | import java.lang.management.ManagementFactory; 8 | import java.util.concurrent.CountDownLatch; 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.atomic.AtomicInteger; 11 | 12 | import static java.lang.Thread.sleep; 13 | import static java.util.concurrent.Executors.newThreadPerTaskExecutor; 14 | 15 | public class OneMillion { 16 | 17 | public static final int TOTAL = 1000_000; 18 | 19 | public static void main(String[] args) throws IOException, InterruptedException { 20 | var counter = new AtomicInteger(0); 21 | long mem0 = getUsedMemory(); 22 | CountDownLatch pause = new CountDownLatch(1); 23 | CountDownLatch started = new CountDownLatch(TOTAL); 24 | try (var virtual = Executors.newVirtualThreadPerTaskExecutor()) { 25 | // try (var virtual = newThreadPerTaskExecutor(Thread::new)) { 26 | for (int i = 0; i < TOTAL; i++) { 27 | virtual.submit(new Runnable() { 28 | @Override 29 | @SneakyThrows 30 | public void run() { 31 | // Thread.currentThread().isVirtual() 32 | counter.incrementAndGet(); 33 | started.countDown(); 34 | pause.await(); 35 | counter.decrementAndGet(); 36 | } 37 | }); 38 | } 39 | System.out.println("Waiting for all tasks to start..."); 40 | started.await(); 41 | System.out.println("All tasks submitted"); 42 | System.out.println("Now running threads: " + counter.get()); 43 | long mem1 = getUsedMemory(); 44 | System.out.println("Delta Memory: " + (mem1 - mem0) / 1024 + " KB, init=" + mem0 / 1024 + " KB"); 45 | System.out.println("Delta / thread: " + (mem1 - mem0) / TOTAL + " bytes"); 46 | System.out.println("processID (PID) =" + ManagementFactory.getRuntimeMXBean().getName()); 47 | System.out.println("Hit Enter to end"); 48 | System.in.read(); 49 | pause.countDown(); 50 | } 51 | System.out.println("All threads ended: " + counter.get()); 52 | } 53 | 54 | @SneakyThrows 55 | public static long getUsedMemory() { 56 | // System.gc(); // to free the intermediary allocated [] when ArrayList grows 57 | sleep(10); 58 | return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/structuredconcurrency/Workshop.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.structuredconcurrency; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import java.util.List; 12 | import java.util.stream.Stream; 13 | 14 | @Slf4j 15 | @RestController 16 | @SpringBootApplication 17 | public class Workshop { 18 | @Component 19 | @SuppressWarnings("unchecked") 20 | public static class ApiClient { 21 | public List getBookingOffers(int providerId) { 22 | return new RestTemplate().getForObject("http://localhost:9999/booking-offers-" + providerId, List.class); 23 | } 24 | 25 | public String getWeather() { 26 | return new RestTemplate().getForObject("http://localhost:9999/weather", String.class); 27 | } 28 | } 29 | 30 | @Autowired 31 | ApiClient apiClient; 32 | 33 | // TODO call getBookingOffers and getWeather in p01_parallel. 34 | @GetMapping("parallel") 35 | public BookingOffersDto p01_parallel() throws InterruptedException { 36 | List offers = apiClient.getBookingOffers(1); 37 | String weather = apiClient.getWeather(); 38 | return new BookingOffersDto(offers, weather); 39 | } 40 | 41 | // TODO call getBookingOffers and getWeather in p01_parallel. <-- idem as above 42 | // After 500 milliseconds (aka p02_timeout), 43 | // - throw error if getBookingOffers did not complete 44 | // - default weather to "Probably Sunny" if getWeather did not complete 45 | @GetMapping("timeout") 46 | public BookingOffersDto p02_timeout() throws InterruptedException { 47 | return null; // copy-paste from above and edit... 48 | } 49 | 50 | // TODO run all 51 | @GetMapping("timely-offers") 52 | public BookingOffersDto p03_timelyOffers() throws InterruptedException { 53 | List offers1 = apiClient.getBookingOffers(1); 54 | List offers2 = apiClient.getBookingOffers(2); 55 | String weather = apiClient.getWeather(); 56 | List allOffers = Stream.concat(offers1.stream(), offers2.stream()).toList(); 57 | return new BookingOffersDto(allOffers, weather); 58 | } 59 | 60 | record BookingOffersDto(List offers, String weather) { 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/util/RunMonitor.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.util; 2 | 3 | import org.apache.tomcat.util.threads.VirtualThreadExecutor; 4 | 5 | import java.util.Collections; 6 | import java.util.Map; 7 | import java.util.TreeMap; 8 | import java.util.concurrent.Executors; 9 | 10 | 11 | import static java.lang.System.currentTimeMillis; 12 | import static victor.training.java.virtualthread.util.DisplayVTNameInLogback.friendlyVTName; 13 | 14 | public class RunMonitor { 15 | private final Map taskCompletionTimes = Collections.synchronizedMap(new TreeMap<>()); 16 | private final long tSubmit = currentTimeMillis(); 17 | 18 | // --------- supporting code ------------ 19 | public Runnable run(int taskId, Runnable runnable) { 20 | return () -> { 21 | long tStart = currentTimeMillis(); 22 | String startThreadName = Thread.currentThread().toString(); 23 | runnable.run(); 24 | String endThreadName = Thread.currentThread().toString(); 25 | long tEnd = currentTimeMillis(); 26 | String hop = startThreadName.equals(endThreadName) ? "❌No hop" : 27 | "✅Hopped from " + friendlyVTName(startThreadName) 28 | + " to " + friendlyVTName(endThreadName); 29 | var prev = taskCompletionTimes.put(taskId, 30 | new ExecutionTimeframe(tStart - tSubmit, tEnd - tSubmit, '*', hop)); 31 | if (prev != null) { 32 | throw new IllegalArgumentException("Task ID already exists: " + taskId); 33 | } 34 | }; 35 | } 36 | 37 | public void printExecutionTimes() { 38 | long max = taskCompletionTimes.values().stream().mapToLong(ExecutionTimeframe::end).max() 39 | .orElseThrow(() -> new IllegalStateException("Tasks finished too fast! Please add more work.")); 40 | double r = 50d / max; 41 | for (Integer taskId : taskCompletionTimes.keySet()) { 42 | ExecutionTimeframe t = taskCompletionTimes.get(taskId); 43 | String spaces = " ".repeat((int) (t.start() * r)); 44 | String action = ("" + t.symbol).repeat((int) ((t.end() - t.start()) * r)); 45 | String trail = " ".repeat(50 - spaces.length() - action.length() - 1); 46 | System.out.printf("Task %02d: %s%s%s ->> %s%n", taskId, spaces, action,trail, t.hop); 47 | } 48 | System.out.printf("Total runtime = %d millis for %d tasks on a machine with %d CPUs%n", 49 | currentTimeMillis()-tSubmit, 50 | taskCompletionTimes.size(), 51 | Runtime.getRuntime().availableProcessors()); 52 | } 53 | 54 | record ExecutionTimeframe(long start, long end, char symbol, String hop) { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/template/Template2_Export.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.template; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | 5 | import java.io.File; 6 | import java.io.FileWriter; 7 | import java.io.Writer; 8 | import java.util.List; 9 | import java.util.Objects; 10 | 11 | @RequiredArgsConstructor 12 | public class Template2_Export { 13 | private final FileExporter exporter; 14 | 15 | public void exportOrders() throws Exception { 16 | exporter.exportOrders(); 17 | } 18 | 19 | public void exportProducts() throws Exception { 20 | // TODO 'the same way you did the export of orders' 21 | // RUN UNIT TESTS! 22 | } 23 | } 24 | 25 | 26 | class FileExporter { 27 | private final OrderRepo orderRepo; 28 | private final File exportFolder; 29 | 30 | public FileExporter(OrderRepo orderRepo, File exportFolder) { 31 | this.orderRepo = orderRepo; 32 | this.exportFolder = exportFolder; 33 | } 34 | 35 | public File exportOrders() { 36 | File file = new File(exportFolder, "orders.csv"); 37 | long t0 = System.currentTimeMillis(); 38 | try (Writer writer = new FileWriter(file)) { 39 | System.out.println("Starting export to: " + file.getAbsolutePath()); 40 | 41 | writer.write("OrderID;Date\n"); 42 | 43 | for (Order order : orderRepo.findByActiveTrue()) { 44 | String csv = order.id() + ";" + order.customerId() + ";" + order.amount() + "\n"; 45 | writer.write(csv); 46 | } 47 | 48 | System.out.println("File export completed: " + file.getAbsolutePath()); 49 | return file; 50 | } catch (Exception e) { 51 | System.out.println("Pretend: Send Error Notification Email"); // TODO CR: only for export orders, not for products 52 | throw new RuntimeException("Error exporting data", e); 53 | } finally { 54 | System.out.println("Pretend: Metrics: Export finished in: " + (System.currentTimeMillis() - t0)); 55 | } 56 | } 57 | 58 | public String escapeCell(Object cellValue) { 59 | if (cellValue instanceof String s) { 60 | if (!s.contains("\n")) return s; 61 | return "\"" + s.replace("\"", "\"\"") + "\""; 62 | } else { 63 | return Objects.toString(cellValue); 64 | } 65 | } 66 | } 67 | record Order(Long id, Long customerId, Double amount) { 68 | } 69 | interface OrderRepo { 70 | List findByActiveTrue(); // 1 Mln orders ;) 71 | } 72 | 73 | interface ProductRepo { 74 | List findAll(); 75 | } 76 | 77 | record Product(Long id, String name, Double price, String imageUrl) { 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/patterns/proxy/LoggingAspect.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.patterns.proxy; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.aspectj.lang.ProceedingJoinPoint; 8 | import org.aspectj.lang.annotation.Around; 9 | import org.aspectj.lang.annotation.Aspect; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.io.InputStream; 14 | import java.io.OutputStream; 15 | import java.util.stream.Stream; 16 | 17 | import static java.util.stream.Collectors.joining; 18 | 19 | @Slf4j 20 | @Aspect 21 | @Component 22 | public class LoggingAspect { 23 | private final ObjectMapper jackson = new ObjectMapper(); 24 | 25 | @PostConstruct 26 | public void configureMapper() { 27 | if (log.isTraceEnabled()) { 28 | log.trace("JSON serialization will be indented"); 29 | jackson.enable(SerializationFeature.INDENT_OUTPUT); 30 | } 31 | } 32 | 33 | @Around("@within(victor.training.java.patterns.proxy.Facade))") // all methods inside classes annotated with @Facade 34 | // @Around("@annotation(victor.training.spring.aspects.LoggedMethod))") // all methods annotated with @LoggedMethod 35 | // @Around("execution(* org.springframework.data.jpa.repository.JpaRepository+.*(..))") // all subtypes of JpaRepository 36 | // @Around("execution(* ..*.get*(..))") // all methods starting with "get" everywhere!! = naming convention = dangerous 37 | public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { 38 | 39 | if (log.isDebugEnabled()) { 40 | String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName(); 41 | String currentUsername = "SecurityContextHolder.getContext().getName()"; // TODO 42 | String argListConcat = Stream.of(joinPoint.getArgs()).map(this::jsonify).collect(joining(",")); 43 | log.debug("Invoking {}(..) (user:{}): {}", methodName, currentUsername, argListConcat); 44 | } 45 | 46 | try { 47 | Object returnedObject = joinPoint.proceed(); // allow the call to propagate to original (intercepted) method 48 | 49 | if (log.isDebugEnabled()) { 50 | log.debug("Returned value: {}", jsonify(returnedObject)); 51 | } 52 | return returnedObject; 53 | } catch (Exception e) { 54 | log.error("Threw exception {}: {}", e.getClass(), e.getMessage()); 55 | throw e; 56 | } 57 | } 58 | 59 | private String jsonify(Object object) { 60 | if (object == null) { 61 | return ""; 62 | } else if (object instanceof OutputStream) { 63 | return ""; 64 | } else if (object instanceof InputStream) { 65 | return ""; 66 | } else { 67 | try { 68 | return jackson.writeValueAsString(object); 69 | } catch (JsonProcessingException e) { 70 | log.warn("Could not serialize as JSON (stacktrace on TRACE): " + e); 71 | log.trace("Cannot serialize value: " + e, e); 72 | return ""; 73 | } 74 | } 75 | } 76 | 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/test/java/victor/training/java/virtualthread/VTPinningTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import jdk.jfr.Configuration; 4 | import jdk.jfr.EventType; 5 | import jdk.jfr.FlightRecorder; 6 | import jdk.jfr.Recording; 7 | import jdk.jfr.consumer.RecordedEvent; 8 | import jdk.jfr.consumer.RecordingFile; 9 | import lombok.SneakyThrows; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.io.IOException; 13 | import java.nio.file.Path; 14 | import java.text.ParseException; 15 | import java.time.Instant; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | public class VTPinningTest { 20 | 21 | private static Recording rec; 22 | 23 | public static void main(String[] args) { 24 | try { 25 | activate(); 26 | } catch (IOException | ParseException e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | 31 | public static synchronized void activate() throws IOException, ParseException { 32 | Configuration conf = Configuration.getConfiguration("default"); 33 | rec = new Recording(conf); 34 | configureEvents(rec); 35 | // disable disk writes 36 | rec.setToDisk(true); 37 | rec.start(); 38 | 39 | pinning(); 40 | 41 | rec.stop(); 42 | Path jfrPath = Path.of("recording-%s.jfr".formatted(System.currentTimeMillis())); 43 | rec.dump(jfrPath); 44 | System.out.println("END"); 45 | 46 | read(jfrPath); 47 | } 48 | 49 | @SneakyThrows 50 | private static void read(Path jfrPath) { 51 | 52 | RecordingFile rec = new RecordingFile(jfrPath); 53 | 54 | while (rec.hasMoreEvents()) { 55 | RecordedEvent event = rec.readEvent(); 56 | if ("jdk.VirtualThreadPinned".equals(event.getEventType().getName())) { 57 | Instant startTime = event.getStartTime(); 58 | System.out.println("Pinned at: " + startTime + " event: " + event); 59 | } 60 | } 61 | 62 | rec.close(); 63 | } 64 | 65 | private static void configureEvents(Recording rec) { 66 | 67 | for (EventType et : FlightRecorder.getFlightRecorder().getEventTypes()) { 68 | 69 | if (isEnabledForCrachDump(et)) { 70 | 71 | rec.enable(et.getName()); 72 | 73 | } 74 | 75 | } 76 | 77 | } 78 | 79 | private static boolean isEnabledForCrachDump(EventType et) { 80 | System.out.println("Enabling event type: " + et.getName()); 81 | return true; 82 | } 83 | 84 | public void dump(Path filename) throws IOException { 85 | 86 | rec.dump(filename); 87 | 88 | } 89 | 90 | @SneakyThrows 91 | @Test 92 | void experiment() { 93 | pinning(); 94 | } 95 | 96 | @SneakyThrows 97 | public static Object pinning() { 98 | try (ExecutorService virtualThreads = Executors.newVirtualThreadPerTaskExecutor()) { 99 | return virtualThreads.submit(() -> method()).get(); 100 | } 101 | } 102 | 103 | @SneakyThrows 104 | public static void method() { 105 | synchronized (VTPinningTest.class) { 106 | System.out.println("Start in " + Thread.currentThread().threadId()); 107 | Thread.sleep(2000); 108 | System.out.println("End"); 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/structuredconcurrency/WorkshopSolved.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.structuredconcurrency; 2 | 3 | 4 | import java.time.Instant; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure; 8 | import java.util.concurrent.StructuredTaskScope.Subtask; 9 | import java.util.concurrent.TimeoutException; 10 | import java.util.stream.Stream; 11 | 12 | import static java.util.concurrent.StructuredTaskScope.Subtask.State.SUCCESS; 13 | 14 | public class WorkshopSolved extends Workshop { 15 | public BookingOffersDto p01_parallel() throws InterruptedException { 16 | try (ShutdownOnFailure scope = new ShutdownOnFailure()) { 17 | Subtask> futureOffers = scope.fork(() -> apiClient.getBookingOffers(1)); 18 | Subtask futureWeather = scope.fork(() -> apiClient.getWeather()); 19 | 20 | scope.join(); 21 | 22 | return new BookingOffersDto(futureOffers.get(), futureWeather.get()); 23 | } 24 | } 25 | 26 | public BookingOffersDto p02_timeout() throws InterruptedException { 27 | try (ShutdownOnFailure scope = new ShutdownOnFailure()) { 28 | Subtask> futureOffers = scope.fork(() -> apiClient.getBookingOffers(1)); 29 | Subtask futureWeather = scope.fork(() -> apiClient.getWeather()); 30 | 31 | try { 32 | Instant deadline = Instant.now().plusMillis(500); 33 | scope.joinUntil(deadline); 34 | return new BookingOffersDto(futureOffers.get(), futureWeather.get()); 35 | } catch (TimeoutException e) { 36 | if (futureOffers.state() == Subtask.State.SUCCESS) { 37 | return new BookingOffersDto(futureOffers.get(), "Probably Sunny"); 38 | } else { 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | } 43 | } 44 | 45 | public BookingOffersDto p03_timelyOffers() throws InterruptedException { 46 | try (ShutdownOnFailure scope = new ShutdownOnFailure()) { 47 | Subtask> futureOffers1 = scope.fork(() -> apiClient.getBookingOffers(1)); 48 | Subtask> futureOffers2 = scope.fork(() -> apiClient.getBookingOffers(2)); 49 | Subtask futureWeather = scope.fork(() -> apiClient.getWeather()); 50 | 51 | try { 52 | Instant deadline = Instant.now().plusMillis(500); 53 | scope.joinUntil(deadline); 54 | 55 | // all 3 subtasks completed successfully 56 | List allOffers = Stream.concat(futureOffers1.get().stream(), futureOffers2.get().stream()).toList(); 57 | return new BookingOffersDto(allOffers, futureWeather.get()); 58 | } catch (TimeoutException e) { 59 | List allOffers = new ArrayList<>(); 60 | if (futureOffers1.state()==SUCCESS) { 61 | allOffers.addAll(futureOffers1.get()); 62 | } 63 | if (futureOffers2.state()==SUCCESS) { 64 | allOffers.addAll(futureOffers2.get()); 65 | } 66 | if (allOffers.isEmpty()) throw new RuntimeException("No offer received in time", e); 67 | 68 | String weather = futureWeather.state()==SUCCESS ? futureWeather.get() : "Probably Sunny"; 69 | 70 | return new BookingOffersDto(allOffers, weather); 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/experiments/Experiment.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.experiments; 2 | 3 | import lombok.SneakyThrows; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.apache.http.HttpEntity; 6 | import org.apache.http.client.methods.HttpGet; 7 | import org.apache.http.util.EntityUtils; 8 | import org.awaitility.Awaitility; 9 | import org.springframework.web.client.RestTemplate; 10 | import victor.training.java.Util; 11 | import victor.training.java.virtualthread.util.RunMonitor; 12 | 13 | import java.math.BigInteger; 14 | import java.net.URI; 15 | import java.net.http.HttpClient; 16 | import java.util.concurrent.Executors; 17 | 18 | import static java.math.BigInteger.ZERO; 19 | import static java.math.BigInteger.valueOf; 20 | 21 | @Slf4j 22 | public class Experiment { 23 | 24 | public static void main() throws Exception { 25 | Util.sleepMillis(1000); // allow the SpringApp to restart 26 | RunMonitor monitor = new RunMonitor(); 27 | try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { 28 | for (int taskId = 0; taskId < 3; taskId++) { 29 | Runnable work = () -> { 30 | // io(); // add a prior warmup call 31 | // cpu(); 32 | locks(); 33 | }; 34 | executor.submit(monitor.run(taskId, work)); 35 | } 36 | 37 | // other parallel tasks: 38 | // IntStream.range(30, 40).forEach(taskId -> executor.submit(monitor(taskId, First::io))); 39 | System.out.println("Waiting for tasks to finish..."); 40 | } 41 | monitor.printExecutionTimes(); 42 | } 43 | 44 | private static final HttpClient javaClient = HttpClient.newHttpClient(); 45 | // private static final HttpClient javaClient = HttpClient.newBuilder().executor(Executors.newVirtualThreadPerTaskExecutor()).build(); 46 | 47 | private static final RestTemplate restTemplate = new RestTemplate(); 48 | 49 | private static final org.apache.http.client.HttpClient apacheClient = org.apache.http.impl.client.HttpClients.createDefault(); 50 | 51 | @SneakyThrows 52 | private static void io() { 53 | URI uri = URI.create("http://localhost:8080/call"); 54 | Thread.sleep(100); // pretend a network call: 55 | 56 | // var result = restTemplate.getForObject(uri, String.class); 57 | 58 | // HttpRequest request = HttpRequest.newBuilder().uri(uri).GET().build(); 59 | // var result = javaClient.send(request, HttpResponse.BodyHandlers.ofString()).body(); 60 | 61 | // HttpEntity entity = apacheClient.execute(new HttpGet(uri)).getEntity(); 62 | // var result = EntityUtils.toString(entity); 63 | 64 | // log.info("Fetched {}", result); // IO operation: unmounts PT 65 | } 66 | // TODO throttle pressure on external services using a Semaphore? 67 | 68 | /* ============ CPU ============= */ 69 | public static long blackHole; 70 | 71 | public static void cpu() { 72 | BigInteger res = ZERO; 73 | for (int j = 0; j < 10_000_000; j++) { // decrease this number for slower machines 74 | res = res.add(valueOf(j).sqrt()); 75 | } 76 | blackHole = res.longValue(); 77 | // TODO Fix#1: -Djdk.virtualThreadScheduler.parallelism=20 78 | // TODO Fix#2: Thread.yield 'every now and then' 79 | } 80 | 81 | /* ============ LOCKS ============= */ 82 | static int sharedMutable; 83 | 84 | public static synchronized void locks() { 85 | sharedMutable++; 86 | Util.sleepMillis(100); // long operation (eg network) in sync block 87 | 88 | // TODO add to VM options: -Djdk.tracePinnedThreads=full 89 | // TODO run with JFR profiler and observe "Virtual Thread Pinned" events in the .jfr recording 90 | // TODO run ExperimentTest @Test @ShouldNotPin 91 | // TODO Fix: Refactor to ReentrantLock 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | org.springframework.boot 6 | spring-boot-starter-parent 7 | 3.2.1 8 | 9 | 10 | 11 | victor.training 12 | java-latest 13 | 1.0 14 | 15 | 21 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-web 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-jpa 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-devtools 30 | runtime 31 | true 32 | 33 | 34 | com.h2database 35 | h2 36 | runtime 37 | 38 | 39 | org.jooq 40 | jool 41 | 0.9.14 42 | 43 | 44 | org.springframework.boot 45 | spring-boot-starter-validation 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | compile 51 | 52 | 53 | 54 | io.gatling.highcharts 55 | gatling-charts-highcharts 56 | 3.9.5 57 | test 58 | 59 | 60 | 61 | com.google.guava 62 | guava 63 | 32.1.3-jre 64 | 65 | 66 | org.projectlombok 67 | lombok 68 | 69 | 70 | 71 | org.springframework.cloud 72 | spring-cloud-starter-contract-stub-runner 73 | 74 | 75 | 76 | me.escoffier.loom 77 | loom-unit 78 | 0.3.0 79 | test 80 | 81 | 82 | 83 | 84 | 85 | org.springframework.cloud 86 | spring-cloud-dependencies 87 | 2023.0.0 88 | pom 89 | import 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | org.apache.maven.plugins 98 | maven-compiler-plugin 99 | 100 | 101 | --enable-preview 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /src/main/java/victor/training/java/virtualthread/VirtualThreadsApi.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.client.RestTemplate; 7 | 8 | import java.time.Instant; 9 | import java.util.concurrent.*; 10 | import java.util.concurrent.StructuredTaskScope.ShutdownOnFailure; 11 | import java.util.concurrent.StructuredTaskScope.Subtask; 12 | 13 | import static java.lang.System.currentTimeMillis; 14 | import static java.util.concurrent.CompletableFuture.supplyAsync; 15 | import static java.util.concurrent.TimeUnit.SECONDS; 16 | 17 | //@RestController 18 | @Slf4j 19 | @RequiredArgsConstructor 20 | public class VirtualThreadsApi { 21 | private final RestTemplate restTemplate; 22 | private final Executor virtualExecutor; 23 | 24 | @GetMapping("/warmup") 25 | public Beer warmup() throws InterruptedException { 26 | Thread.sleep(500); 27 | return new Beer("warm"); // initial performance comparison between PT and VT 28 | } 29 | 30 | @GetMapping("/sequential") 31 | public Beer sequential() { 32 | log.info("Start"); // TODO vrentea 12.09.2024: have this printed 33 | UserPreferences preferences = fetchPreferences(); 34 | log.info("Preferences: {}", preferences); 35 | Beer beer = fetchBeer(preferences.favoriteBeerType()); 36 | log.info("Got beer: {}", beer); 37 | return beer; 38 | } 39 | 40 | @GetMapping("/parallel-non-blocking") 41 | public CompletableFuture drinkCF() { 42 | long t0 = currentTimeMillis(); 43 | // ⚠️ For a proper performance comparison, reimplement this with WebClient...toFuture on a spring-webflux project 44 | CompletableFuture beerCF = supplyAsync(() -> fetchBeer("blond")); 45 | CompletableFuture vodkaCF = supplyAsync(() -> fetchVodka()); 46 | 47 | 48 | 49 | CompletableFuture dillyCF = beerCF.thenCombine(vodkaCF, DillyDilly::new); 50 | log.info("HTTP Thread released in {} ms", currentTimeMillis() - t0); 51 | // ✅ HTTP Thread released immediately, but ❌ hard to maintain 52 | return dillyCF; 53 | } 54 | 55 | // parallel work using Virtual Threads 56 | 57 | @GetMapping("/parallel-virtual") 58 | public DillyDilly blockingOnFutures() throws InterruptedException, ExecutionException, TimeoutException { 59 | CompletableFuture beerCF = supplyAsync(() -> fetchBeer("blond"), virtualExecutor); 60 | CompletableFuture vodkaCF = supplyAsync(() -> fetchVodka(), virtualExecutor); 61 | 62 | Beer beer = beerCF.get(2, SECONDS); // blocking a light VT is OK 63 | Vodka vodka = vodkaCF.get(2, SECONDS); 64 | // ❌ the tasks are NOT interrupted if: if client cancels, not if the other task fails 65 | // ❌ JFR profiler can't trace parents not show the subtasks 66 | return new DillyDilly(beer, vodka); 67 | } 68 | // Structured Concurrency (not yet production-ready in Java 21 LTS) 69 | 70 | @GetMapping("/parallel-scope") 71 | public DillyDilly structuredConcurrency() throws InterruptedException, ExecutionException, TimeoutException { 72 | try (var scope = new ShutdownOnFailure()) { 73 | Subtask beerTask = scope.fork(() -> fetchBeer("blond")); // +1 child VT 74 | Subtask vodkaTask = scope.fork(() -> fetchVodka()); // +1 child VT 75 | 76 | scope.joinUntil(Instant.now().plusSeconds(10)) // block the parent VT until all children complete 77 | .throwIfFailed(); // throw exception if any subtasks failed 78 | return new DillyDilly(beerTask.get(), vodkaTask.get()); 79 | } 80 | } 81 | private UserPreferences fetchPreferences() { 82 | return restTemplate.getForObject("http://localhost:9999/api/user-preferences", UserPreferences.class); 83 | } 84 | 85 | private Vodka fetchVodka() { 86 | return restTemplate.getForObject("http://localhost:9999/vodka", Vodka.class); 87 | } 88 | 89 | private Beer fetchBeer(String type) { 90 | return restTemplate.getForObject("http://localhost:9999/api/beer/" + type, Beer.class); 91 | } 92 | 93 | public record Beer(String type) { 94 | } 95 | 96 | public record DillyDilly(Beer beer, Vodka vodka) { 97 | } 98 | 99 | public record UserPreferences(String favoriteBeerType, boolean iceInVodka) { 100 | } 101 | 102 | public record Vodka(String type) { 103 | } 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/test/java/victor/training/java/virtualthread/structuredconcurrency/WorkshopTest.java: -------------------------------------------------------------------------------- 1 | package victor.training.java.virtualthread.structuredconcurrency; 2 | 3 | import org.junit.jupiter.api.MethodOrderer.MethodName; 4 | import org.junit.jupiter.api.Test; 5 | import org.junit.jupiter.api.TestMethodOrder; 6 | import org.junit.jupiter.api.Timeout; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.internal.stubbing.answers.AnswersWithDelay; 11 | import org.mockito.internal.stubbing.answers.Returns; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import victor.training.java.virtualthread.structuredconcurrency.Workshop; 14 | import victor.training.java.virtualthread.structuredconcurrency.Workshop.ApiClient; 15 | import victor.training.java.virtualthread.structuredconcurrency.Workshop.BookingOffersDto; 16 | 17 | import java.util.List; 18 | import java.util.concurrent.TimeoutException; 19 | 20 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatThrownBy; 23 | import static org.mockito.Mockito.when; 24 | 25 | // Note: if you get the exception java.lang.NoClassDefFoundError: jdk/incubator/concurrent/StructuredTaskScope$ShutdownOnFailure 26 | // you need to add '--add-modules jdk.incubator.concurrent' to the launch profile of the tests 27 | @TestMethodOrder(MethodName.class) 28 | @ExtendWith(MockitoExtension.class) 29 | class WorkshopTest { 30 | @Mock 31 | ApiClient apiClient; 32 | @InjectMocks 33 | Workshop workshop; 34 | 35 | @Test 36 | @Timeout(value = 150, unit = MILLISECONDS) 37 | void p01_parallel() throws InterruptedException { 38 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(100, new Returns(List.of("offer")))); 39 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(100, new Returns("Rain"))); 40 | 41 | BookingOffersDto r = workshop.p01_parallel(); 42 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer"), "Rain")); 43 | } 44 | 45 | @Test 46 | @Timeout(value = 600, unit = MILLISECONDS) 47 | void p02_timeoutOnTime() throws InterruptedException { 48 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(100, new Returns(List.of("offer")))); 49 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(100, new Returns("Rain"))); 50 | 51 | BookingOffersDto r = workshop.p02_timeout(); 52 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer"), "Rain")); 53 | } 54 | 55 | @Test 56 | @Timeout(value = 600, unit = MILLISECONDS) 57 | void p02_timeoutTimeoutsWeather_throws() throws InterruptedException { 58 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(100, new Returns(List.of("offer")))); 59 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(700, new Returns("Rain"))); 60 | 61 | BookingOffersDto r = workshop.p02_timeout(); 62 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer"), "Probably Sunny")); 63 | } 64 | 65 | @Test 66 | @Timeout(value = 600, unit = MILLISECONDS) 67 | void p02_timeoutTimeoutsOffers_defaultsWeather() throws InterruptedException { 68 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(600, new Returns(List.of("offer")))); 69 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(200, new Returns("Rain"))); 70 | 71 | assertThatThrownBy(() -> workshop.p02_timeout()) 72 | .hasCauseInstanceOf(TimeoutException.class); 73 | } 74 | 75 | @Test 76 | @Timeout(value = 600, unit = MILLISECONDS) 77 | void p03_timelyOffers_onTime() throws InterruptedException { 78 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer1")))); 79 | when(apiClient.getBookingOffers(2)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer2")))); 80 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(300, new Returns("Rain"))); 81 | 82 | BookingOffersDto r = workshop.p03_timelyOffers(); 83 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer1","offer2"), "Rain")); 84 | } 85 | 86 | @Test 87 | @Timeout(value = 600, unit = MILLISECONDS) 88 | void p03_timelyOffers_defaultsWeather() throws InterruptedException { 89 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer1")))); 90 | when(apiClient.getBookingOffers(2)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer2")))); 91 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(900, new Returns("Rain"))); 92 | 93 | BookingOffersDto r = workshop.p03_timelyOffers(); 94 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer1","offer2"), "Probably Sunny")); 95 | } 96 | @Test 97 | @Timeout(value = 600, unit = MILLISECONDS) 98 | void p03_timelyOffers_servesOffer1() throws InterruptedException { 99 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer1")))); 100 | when(apiClient.getBookingOffers(2)).thenAnswer(new AnswersWithDelay(1400, new Returns(List.of("offer2")))); 101 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(300, new Returns("Rain"))); 102 | 103 | BookingOffersDto r = workshop.p03_timelyOffers(); 104 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer1"), "Rain")); 105 | } 106 | @Test 107 | @Timeout(value = 600, unit = MILLISECONDS) 108 | void p03_timelyOffers_servesOffer2() throws InterruptedException { 109 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(1400, new Returns(List.of("offer1")))); 110 | when(apiClient.getBookingOffers(2)).thenAnswer(new AnswersWithDelay(400, new Returns(List.of("offer2")))); 111 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(300, new Returns("Rain"))); 112 | 113 | BookingOffersDto r = workshop.p03_timelyOffers(); 114 | assertThat(r).isEqualTo(new BookingOffersDto(List.of("offer2"), "Rain")); 115 | } 116 | @Test 117 | @Timeout(value = 600, unit = MILLISECONDS) 118 | void p03_timelyOffers_throws_whenNoOffersFound() throws InterruptedException { 119 | when(apiClient.getBookingOffers(1)).thenAnswer(new AnswersWithDelay(1400, new Returns(List.of("offer1")))); 120 | when(apiClient.getBookingOffers(2)).thenAnswer(new AnswersWithDelay(1400, new Returns(List.of("offer2")))); 121 | when(apiClient.getWeather()).thenAnswer(new AnswersWithDelay(300, new Returns("Rain"))); 122 | 123 | assertThatThrownBy(() -> workshop.p03_timelyOffers()) 124 | .hasMessageContaining("No offer received"); 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/test/resources/gatling.conf: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Gatling Configuration # 3 | ######################### 4 | 5 | # This file contains all the settings configurable for Gatling with their default values 6 | 7 | gatling { 8 | core { 9 | #outputDirectoryBaseName = "" # The prefix for each simulation result folder (then suffixed by the report generation timestamp) 10 | runDescription = "yolo" # The description for this simulation run, displayed in each report 11 | #encoding = "utf-8" # Encoding to use throughout Gatling for file and string manipulation 12 | #simulationClass = "" # The FQCN of the simulation to run (when used in conjunction with noReports, the simulation for which assertions will be validated) 13 | #elFileBodiesCacheMaxCapacity = 200 # Cache size for request body EL templates, set to 0 to disable 14 | #rawFileBodiesCacheMaxCapacity = 200 # Cache size for request body Raw templates, set to 0 to disable 15 | #rawFileBodiesInMemoryMaxSize = 1000 # Below this limit, raw file bodies will be cached in memory 16 | #pebbleFileBodiesCacheMaxCapacity = 200 # Cache size for request body Peeble templates, set to 0 to disable 17 | #feederAdaptiveLoadModeThreshold = 100 # File size threshold (in MB). Below load eagerly in memory, above use batch mode with default buffer size 18 | #shutdownTimeout = 10000 # Milliseconds to wait for the actor system to shutdown 19 | extract { 20 | regex { 21 | #cacheMaxCapacity = 200 # Cache size for the compiled regexes, set to 0 to disable caching 22 | } 23 | xpath { 24 | #cacheMaxCapacity = 200 # Cache size for the compiled XPath queries, set to 0 to disable caching 25 | } 26 | jsonPath { 27 | #cacheMaxCapacity = 200 # Cache size for the compiled jsonPath queries, set to 0 to disable caching 28 | } 29 | css { 30 | #cacheMaxCapacity = 200 # Cache size for the compiled CSS selectors queries, set to 0 to disable caching 31 | } 32 | } 33 | directory { 34 | #simulations = user-files/simulations # Directory where simulation classes are located (for bundle packaging only) 35 | #resources = user-files/resources # Directory where resources, such as feeder files and request bodies are located (for bundle packaging only) 36 | #reportsOnly = "" # If set, name of report folder to look for in order to generate its report 37 | #binaries = "" # If set, name of the folder where compiles classes are located: Defaults to GATLING_HOME/target. 38 | #results = results # Name of the folder where all reports folder are located 39 | } 40 | } 41 | socket { 42 | #connectTimeout = 10000 # Timeout in millis for establishing a TCP socket 43 | #tcpNoDelay = true 44 | #soKeepAlive = false # if TCP keepalive configured at OS level should be used 45 | #soReuseAddress = false 46 | } 47 | netty { 48 | #useNativeTransport = true # if Netty native transport should be used instead of Java NIO 49 | #allocator = "pooled" # switch to unpooled for unpooled ByteBufAllocator 50 | #maxThreadLocalCharBufferSize = 200000 # Netty's default is 16k 51 | } 52 | ssl { 53 | #useOpenSsl = true # if OpenSSL should be used instead of JSSE (only the latter can be debugged with -Djava.net.debug=ssl) 54 | #useOpenSslFinalizers = false # if OpenSSL contexts should be freed with Finalizer or if using RefCounted is fine 55 | #handshakeTimeout = 10000 # TLS handshake p02_timeout in millis 56 | #useInsecureTrustManager = true # Use an insecure TrustManager that trusts all server certificates 57 | #enabledProtocols = [] # Array of enabled protocols for HTTPS, if empty use Netty's defaults 58 | #enabledCipherSuites = [] # Array of enabled cipher suites for HTTPS, if empty enable all available ciphers 59 | #sessionCacheSize = 0 # SSLSession cache size, set to 0 to use JDK's default 60 | #sessionTimeout = 0 # SSLSession p02_timeout in seconds, set to 0 to use JDK's default (24h) 61 | #enableSni = true # When set to true, enable Server Name indication (SNI) 62 | keyStore { 63 | #type = "" # Type of SSLContext's KeyManagers store 64 | #file = "" # Location of SSLContext's KeyManagers store 65 | #password = "" # Password for SSLContext's KeyManagers store 66 | #algorithm = "" # Algorithm used SSLContext's KeyManagers store 67 | } 68 | trustStore { 69 | #type = "" # Type of SSLContext's TrustManagers store 70 | #file = "" # Location of SSLContext's TrustManagers store 71 | #password = "" # Password for SSLContext's TrustManagers store 72 | #algorithm = "" # Algorithm used by SSLContext's TrustManagers store 73 | } 74 | } 75 | charting { 76 | #noReports = false # When set to true, don't generate HTML reports 77 | #maxPlotPerSeries = 1000 # Number of points per graph in Gatling reports 78 | #useGroupDurationMetric = false # Switch group timings from cumulated response time to group duration. 79 | indicators { 80 | #lowerBound = 800 # Lower bound for the requests' response time to track in the reports and the console summary 81 | #higherBound = 1200 # Higher bound for the requests' response time to track in the reports and the console summary 82 | #percentile1 = 50 # Value for the 1st percentile to track in the reports, the console summary and Graphite 83 | #percentile2 = 75 # Value for the 2nd percentile to track in the reports, the console summary and Graphite 84 | #percentile3 = 95 # Value for the 3rd percentile to track in the reports, the console summary and Graphite 85 | #percentile4 = 99 # Value for the 4th percentile to track in the reports, the console summary and Graphite 86 | } 87 | } 88 | http { 89 | #fetchedCssCacheMaxCapacity = 200 # Cache size for CSS parsed content, set to 0 to disable 90 | #fetchedHtmlCacheMaxCapacity = 200 # Cache size for HTML parsed content, set to 0 to disable 91 | #perUserCacheMaxCapacity = 200 # Per virtual user cache size, set to 0 to disable 92 | #warmUpUrl = "https://gatling.io" # The URL to use to warm-up the HTTP stack (blank means disabled) 93 | #enableGA = true # Very light Google Analytics (Gatling and Java version), please support 94 | #pooledConnectionIdleTimeout = 60000 # Timeout in millis for a connection to stay idle in the pool 95 | #requestTimeout = 60000 # Timeout in millis for performing an HTTP request 96 | #enableHostnameVerification = false # When set to true, enable hostname verification: SSLEngine.setHttpsEndpointIdentificationAlgorithm("HTTPS") 97 | dns { 98 | #queryTimeout = 5000 # Timeout in millis of each DNS query in millis 99 | #maxQueriesPerResolve = 6 # Maximum allowed number of DNS queries for a given name resolution 100 | } 101 | } 102 | jms { 103 | #replyTimeoutScanPeriod = 1000 # scan period for timedout reply messages 104 | } 105 | data { 106 | #writers = [console, file] # The list of DataWriters to which Gatling write simulation data (currently supported : console, file, graphite) 107 | console { 108 | #light = false # When set to true, displays a light version without detailed request stats 109 | #writePeriod = 5 # Write interval, in seconds 110 | } 111 | file { 112 | #bufferSize = 8192 # FileDataWriter's internal data buffer size, in bytes 113 | } 114 | leak { 115 | #noActivityTimeout = 30 # Period, in seconds, for which Gatling may have no activity before considering a leak may be happening 116 | } 117 | graphite { 118 | #light = false # only send the all* stats 119 | #host = "localhost" # The host where the Carbon server is located 120 | #port = 2003 # The port to which the Carbon server listens to (2003 is default for plaintext, 2004 is default for pickle) 121 | #protocol = "tcp" # The protocol used to send data to Carbon (currently supported : "tcp", "udp") 122 | #rootPathPrefix = "gatling" # The common prefix of all metrics sent to Graphite 123 | #bufferSize = 8192 # Internal data buffer size, in bytes 124 | #writePeriod = 1 # Write period, in seconds 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /jMeter-project.jmx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | true 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | continue 17 | 18 | false 19 | -1 20 | 21 | 20000 22 | 1 23 | false 24 | 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | 33 | localhost 34 | 8080 35 | 36 | 37 | call 38 | GET 39 | true 40 | false 41 | true 42 | false 43 | 44 | 45 | 46 | 47 | 48 | 49 | false 50 | 51 | saveConfig 52 | 53 | 54 | true 55 | true 56 | true 57 | 58 | true 59 | true 60 | true 61 | true 62 | false 63 | true 64 | true 65 | false 66 | false 67 | false 68 | true 69 | false 70 | false 71 | false 72 | true 73 | 0 74 | true 75 | true 76 | true 77 | true 78 | true 79 | true 80 | 81 | 82 | 83 | 84 | 85 | 86 | false 87 | 88 | saveConfig 89 | 90 | 91 | true 92 | true 93 | true 94 | 95 | true 96 | true 97 | true 98 | true 99 | false 100 | true 101 | true 102 | false 103 | false 104 | false 105 | true 106 | false 107 | false 108 | false 109 | true 110 | 0 111 | true 112 | true 113 | true 114 | true 115 | true 116 | true 117 | 118 | 119 | 120 | 121 | 122 | 123 | false 124 | 125 | saveConfig 126 | 127 | 128 | true 129 | true 130 | true 131 | 132 | true 133 | true 134 | true 135 | true 136 | false 137 | true 138 | true 139 | false 140 | false 141 | false 142 | true 143 | false 144 | false 145 | false 146 | true 147 | 0 148 | true 149 | true 150 | true 151 | true 152 | true 153 | true 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /threads.json: -------------------------------------------------------------------------------- 1 | { 2 | "threadDump": { 3 | "processId": "54966", 4 | "time": "2023-02-06T04:38:17.842528Z", 5 | "runtimeVersion": "19.0.2+7-44", 6 | "threadContainers": [ 7 | { 8 | "container": "", 9 | "parent": null, 10 | "owner": null, 11 | "threads": [ 12 | { 13 | "tid": "8", 14 | "name": "Reference Handler", 15 | "stack": [ 16 | "java.base\/java.lang.ref.Reference.waitForReferencePendingList(Native Method)", 17 | "java.base\/java.lang.ref.Reference.processPendingReferences(Reference.java:245)", 18 | "java.base\/java.lang.ref.Reference$ReferenceHandler.run(Reference.java:207)" 19 | ] 20 | }, 21 | { 22 | "tid": "9", 23 | "name": "Finalizer", 24 | "stack": [ 25 | "java.base\/java.lang.Object.wait0(Native Method)", 26 | "java.base\/java.lang.Object.wait(Object.java:366)", 27 | "java.base\/java.lang.Object.wait(Object.java:339)", 28 | "java.base\/java.lang.ref.NativeReferenceQueue.await(NativeReferenceQueue.java:48)", 29 | "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:158)", 30 | "java.base\/java.lang.ref.NativeReferenceQueue.remove(NativeReferenceQueue.java:89)", 31 | "java.base\/java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:173)" 32 | ] 33 | }, 34 | { 35 | "tid": "10", 36 | "name": "Signal Dispatcher", 37 | "stack": [ 38 | ] 39 | }, 40 | { 41 | "tid": "18", 42 | "name": "Common-Cleaner", 43 | "stack": [ 44 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 45 | "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)", 46 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1759)", 47 | "java.base\/java.lang.ref.ReferenceQueue.await(ReferenceQueue.java:71)", 48 | "java.base\/java.lang.ref.ReferenceQueue.remove0(ReferenceQueue.java:143)", 49 | "java.base\/java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:218)", 50 | "java.base\/jdk.internal.ref.CleanerImpl.run(CleanerImpl.java:140)", 51 | "java.base\/java.lang.Thread.run(Thread.java:1589)", 52 | "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)" 53 | ] 54 | }, 55 | { 56 | "tid": "19", 57 | "name": "Monitor Ctrl-Break", 58 | "stack": [ 59 | "java.base\/sun.nio.ch.SocketDispatcher.read0(Native Method)", 60 | "java.base\/sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:47)", 61 | "java.base\/sun.nio.ch.NioSocketImpl.tryRead(NioSocketImpl.java:251)", 62 | "java.base\/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:302)", 63 | "java.base\/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:340)", 64 | "java.base\/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:789)", 65 | "java.base\/java.net.Socket$SocketInputStream.read(Unknown Source)", 66 | "java.base\/sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:333)", 67 | "java.base\/sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:376)", 68 | "java.base\/sun.nio.cs.StreamDecoder.lockedRead(StreamDecoder.java:219)", 69 | "java.base\/sun.nio.cs.StreamDecoder.read(StreamDecoder.java:173)", 70 | "java.base\/java.io.InputStreamReader.read(InputStreamReader.java:189)", 71 | "java.base\/java.io.BufferedReader.fill(BufferedReader.java:161)", 72 | "java.base\/java.io.BufferedReader.implReadLine(BufferedReader.java:371)", 73 | "java.base\/java.io.BufferedReader.readLine(BufferedReader.java:348)", 74 | "java.base\/java.io.BufferedReader.readLine(BufferedReader.java:437)", 75 | "com.intellij.rt.execution.application.AppMainV2$1.run(AppMainV2.java:56)" 76 | ] 77 | }, 78 | { 79 | "tid": "20", 80 | "name": "Notification Thread", 81 | "stack": [ 82 | ] 83 | }, 84 | { 85 | "tid": "41", 86 | "name": "container-0", 87 | "stack": [ 88 | "java.base\/java.lang.Thread.sleep0(Native Method)", 89 | "java.base\/java.lang.Thread.sleep(Thread.java:465)", 90 | "org.apache.catalina.core.StandardServer.await(StandardServer.java:562)", 91 | "org.springframework.boot.web.embedded.tomcat.TomcatWebServer$1.run(TomcatWebServer.java:197)" 92 | ] 93 | }, 94 | { 95 | "tid": "43", 96 | "name": "File Watcher", 97 | "stack": [ 98 | "java.base\/java.lang.Thread.sleep0(Native Method)", 99 | "java.base\/java.lang.Thread.sleep(Thread.java:465)", 100 | "org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.scan(FileSystemWatcher.java:273)", 101 | "org.springframework.boot.devtools.filewatch.FileSystemWatcher$Watcher.run(FileSystemWatcher.java:263)", 102 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 103 | ] 104 | }, 105 | { 106 | "tid": "45", 107 | "name": "Live Reload Server", 108 | "stack": [ 109 | "java.base\/sun.nio.ch.Net.accept(Native Method)", 110 | "java.base\/sun.nio.ch.NioSocketImpl.accept(NioSocketImpl.java:741)", 111 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:690)", 112 | "java.base\/java.net.ServerSocket.platformImplAccept(ServerSocket.java:655)", 113 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:631)", 114 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:588)", 115 | "java.base\/java.net.ServerSocket.accept(ServerSocket.java:546)", 116 | "org.springframework.boot.devtools.livereload.LiveReloadServer.acceptConnections(LiveReloadServer.java:145)", 117 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 118 | ] 119 | }, 120 | { 121 | "tid": "46", 122 | "name": "http-nio-8080-Poller", 123 | "stack": [ 124 | "java.base\/sun.nio.ch.KQueue.poll(Native Method)", 125 | "java.base\/sun.nio.ch.KQueueSelectorImpl.doSelect(KQueueSelectorImpl.java:125)", 126 | "java.base\/sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:129)", 127 | "java.base\/sun.nio.ch.SelectorImpl.select(SelectorImpl.java:141)", 128 | "org.apache.tomcat.util.net.NioEndpoint$Poller.run(NioEndpoint.java:751)", 129 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 130 | ] 131 | }, 132 | { 133 | "tid": "47", 134 | "name": "http-nio-8080-Acceptor", 135 | "stack": [ 136 | "java.base\/sun.nio.ch.Net.accept(Native Method)", 137 | "java.base\/sun.nio.ch.ServerSocketChannelImpl.implAccept(ServerSocketChannelImpl.java:433)", 138 | "java.base\/sun.nio.ch.ServerSocketChannelImpl.accept(ServerSocketChannelImpl.java:399)", 139 | "org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:520)", 140 | "org.apache.tomcat.util.net.NioEndpoint.serverSocketAccept(NioEndpoint.java:79)", 141 | "org.apache.tomcat.util.net.Acceptor.run(Acceptor.java:128)", 142 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 143 | ] 144 | }, 145 | { 146 | "tid": "48", 147 | "name": "DestroyJavaVM", 148 | "stack": [ 149 | ] 150 | }, 151 | { 152 | "tid": "61", 153 | "name": "Read-Poller", 154 | "stack": [ 155 | "java.base\/sun.nio.ch.KQueue.poll(Native Method)", 156 | "java.base\/sun.nio.ch.KQueuePoller.poll(KQueuePoller.java:66)", 157 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:363)", 158 | "java.base\/sun.nio.ch.Poller.pollLoop(Poller.java:270)", 159 | "java.base\/java.lang.Thread.run(Thread.java:1589)", 160 | "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)" 161 | ] 162 | }, 163 | { 164 | "tid": "62", 165 | "name": "Read-Updater", 166 | "stack": [ 167 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 168 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 169 | "java.base\/java.util.concurrent.LinkedTransferQueue$Node.block(LinkedTransferQueue.java:470)", 170 | "java.base\/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3745)", 171 | "java.base\/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3690)", 172 | "java.base\/java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:669)", 173 | "java.base\/java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:616)", 174 | "java.base\/java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1286)", 175 | "java.base\/sun.nio.ch.Poller.updateLoop(Poller.java:286)", 176 | "java.base\/java.lang.Thread.run(Thread.java:1589)", 177 | "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)" 178 | ] 179 | }, 180 | { 181 | "tid": "63", 182 | "name": "Write-Poller", 183 | "stack": [ 184 | "java.base\/sun.nio.ch.KQueue.poll(Native Method)", 185 | "java.base\/sun.nio.ch.KQueuePoller.poll(KQueuePoller.java:66)", 186 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:363)", 187 | "java.base\/sun.nio.ch.Poller.pollLoop(Poller.java:270)", 188 | "java.base\/java.lang.Thread.run(Thread.java:1589)", 189 | "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)" 190 | ] 191 | }, 192 | { 193 | "tid": "64", 194 | "name": "Write-Updater", 195 | "stack": [ 196 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 197 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 198 | "java.base\/java.util.concurrent.LinkedTransferQueue$Node.block(LinkedTransferQueue.java:470)", 199 | "java.base\/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3745)", 200 | "java.base\/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3690)", 201 | "java.base\/java.util.concurrent.LinkedTransferQueue.awaitMatch(LinkedTransferQueue.java:669)", 202 | "java.base\/java.util.concurrent.LinkedTransferQueue.xfer(LinkedTransferQueue.java:616)", 203 | "java.base\/java.util.concurrent.LinkedTransferQueue.take(LinkedTransferQueue.java:1286)", 204 | "java.base\/sun.nio.ch.Poller.updateLoop(Poller.java:286)", 205 | "java.base\/java.lang.Thread.run(Thread.java:1589)", 206 | "java.base\/jdk.internal.misc.InnocuousThread.run(InnocuousThread.java:186)" 207 | ] 208 | }, 209 | { 210 | "tid": "65", 211 | "name": "Attach Listener", 212 | "stack": [ 213 | "java.base\/java.lang.Thread.getStackTrace(Thread.java:2550)", 214 | "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadToJson(ThreadDumper.java:262)", 215 | "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:237)", 216 | "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:201)", 217 | "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToFile(ThreadDumper.java:115)", 218 | "java.base\/jdk.internal.vm.ThreadDumper.dumpThreadsToJson(ThreadDumper.java:84)" 219 | ] 220 | }, 221 | { 222 | "tid": "66", 223 | "name": "JFR Recorder Thread", 224 | "stack": [ 225 | ] 226 | }, 227 | { 228 | "tid": "67", 229 | "name": "JFR Periodic Tasks", 230 | "stack": [ 231 | "java.base\/java.lang.Object.wait0(Native Method)", 232 | "java.base\/java.lang.Object.wait(Object.java:366)", 233 | "jdk.jfr\/jdk.jfr.internal.PlatformRecorder.takeNap(PlatformRecorder.java:527)", 234 | "jdk.jfr\/jdk.jfr.internal.PlatformRecorder.periodicTask(PlatformRecorder.java:508)", 235 | "jdk.jfr\/jdk.jfr.internal.PlatformRecorder.lambda$startDiskMonitor$1(PlatformRecorder.java:447)", 236 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 237 | ] 238 | }, 239 | { 240 | "tid": "69", 241 | "name": "RMI TCP Accept-0", 242 | "stack": [ 243 | "java.base\/sun.nio.ch.Net.accept(Native Method)", 244 | "java.base\/sun.nio.ch.NioSocketImpl.accept(NioSocketImpl.java:741)", 245 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:690)", 246 | "java.base\/java.net.ServerSocket.platformImplAccept(ServerSocket.java:655)", 247 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:631)", 248 | "java.base\/java.net.ServerSocket.implAccept(ServerSocket.java:588)", 249 | "java.base\/java.net.ServerSocket.accept(ServerSocket.java:546)", 250 | "jdk.management.agent\/sun.management.jmxremote.LocalRMIServerSocketFactory$1.accept(LocalRMIServerSocketFactory.java:52)", 251 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$AcceptLoop.executeAcceptLoop(TCPTransport.java:424)", 252 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$AcceptLoop.run(TCPTransport.java:388)", 253 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 254 | ] 255 | }, 256 | { 257 | "tid": "72", 258 | "name": "JMX server connection timeout 72", 259 | "stack": [ 260 | "java.base\/java.lang.Object.wait0(Native Method)", 261 | "java.base\/java.lang.Object.wait(Object.java:366)", 262 | "java.management\/com.sun.jmx.remote.internal.ServerCommunicatorAdmin$Timeout.run(ServerCommunicatorAdmin.java:171)", 263 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 264 | ] 265 | } 266 | ], 267 | "threadCount": "21" 268 | }, 269 | { 270 | "container": "java.util.concurrent.ScheduledThreadPoolExecutor@6629706c\/jdk.internal.vm.SharedThreadContainer@5facc46f", 271 | "parent": "", 272 | "owner": null, 273 | "threads": [ 274 | { 275 | "tid": "38", 276 | "name": "HikariPool-1 housekeeper", 277 | "stack": [ 278 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 279 | "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)", 280 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1674)", 281 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)", 282 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)", 283 | "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)", 284 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)", 285 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 286 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 287 | ] 288 | } 289 | ], 290 | "threadCount": "1" 291 | }, 292 | { 293 | "container": "java.util.concurrent.ThreadPoolExecutor@2765a9c\/jdk.internal.vm.SharedThreadContainer@14eecdd", 294 | "parent": "", 295 | "owner": null, 296 | "threads": [ 297 | ], 298 | "threadCount": "0" 299 | }, 300 | { 301 | "container": "java.util.concurrent.ThreadPoolExecutor@775e57f8\/jdk.internal.vm.SharedThreadContainer@5f2869dc", 302 | "parent": "", 303 | "owner": null, 304 | "threads": [ 305 | ], 306 | "threadCount": "0" 307 | }, 308 | { 309 | "container": "java.util.concurrent.ScheduledThreadPoolExecutor@34c98c8f\/jdk.internal.vm.SharedThreadContainer@2ae3bce2", 310 | "parent": "", 311 | "owner": null, 312 | "threads": [ 313 | ], 314 | "threadCount": "0" 315 | }, 316 | { 317 | "container": "java.util.concurrent.ThreadPoolExecutor@7be2b58f\/jdk.internal.vm.SharedThreadContainer@13558a6d", 318 | "parent": "", 319 | "owner": null, 320 | "threads": [ 321 | ], 322 | "threadCount": "0" 323 | }, 324 | { 325 | "container": "java.util.concurrent.ThreadPoolExecutor@fdb36d3\/jdk.internal.vm.SharedThreadContainer@658ba4de", 326 | "parent": "", 327 | "owner": null, 328 | "threads": [ 329 | ], 330 | "threadCount": "0" 331 | }, 332 | { 333 | "container": "ForkJoinPool-1\/jdk.internal.vm.SharedThreadContainer@79f1573a", 334 | "parent": "", 335 | "owner": null, 336 | "threads": [ 337 | { 338 | "tid": "51", 339 | "name": "ForkJoinPool-1-worker-2", 340 | "stack": [ 341 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 342 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 343 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)", 344 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 345 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 346 | ] 347 | }, 348 | { 349 | "tid": "53", 350 | "name": "ForkJoinPool-1-worker-4", 351 | "stack": [ 352 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 353 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 354 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)", 355 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 356 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 357 | ] 358 | }, 359 | { 360 | "tid": "55", 361 | "name": "ForkJoinPool-1-worker-6", 362 | "stack": [ 363 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 364 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 365 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)", 366 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 367 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 368 | ] 369 | }, 370 | { 371 | "tid": "57", 372 | "name": "ForkJoinPool-1-worker-8", 373 | "stack": [ 374 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 375 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 376 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)", 377 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 378 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 379 | ] 380 | }, 381 | { 382 | "tid": "58", 383 | "name": "ForkJoinPool-1-worker-9", 384 | "stack": [ 385 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 386 | "java.base\/java.util.concurrent.locks.LockSupport.parkUntil(LockSupport.java:449)", 387 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1889)", 388 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 389 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 390 | ] 391 | }, 392 | { 393 | "tid": "82", 394 | "name": "ForkJoinPool-1-worker-10", 395 | "stack": [ 396 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 397 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 398 | "java.base\/java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1891)", 399 | "java.base\/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1807)", 400 | "java.base\/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:177)" 401 | ] 402 | } 403 | ], 404 | "threadCount": "6" 405 | }, 406 | { 407 | "container": "java.util.concurrent.ScheduledThreadPoolExecutor@6180de75\/jdk.internal.vm.SharedThreadContainer@4ec2e04a", 408 | "parent": "", 409 | "owner": null, 410 | "threads": [ 411 | { 412 | "tid": "39", 413 | "name": "Catalina-utility-1", 414 | "stack": [ 415 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 416 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:371)", 417 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionNode.block(AbstractQueuedSynchronizer.java:506)", 418 | "java.base\/java.util.concurrent.ForkJoinPool.unmanagedBlock(ForkJoinPool.java:3745)", 419 | "java.base\/java.util.concurrent.ForkJoinPool.managedBlock(ForkJoinPool.java:3690)", 420 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:1625)", 421 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1177)", 422 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)", 423 | "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)", 424 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)", 425 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 426 | "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)", 427 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 428 | ] 429 | }, 430 | { 431 | "tid": "40", 432 | "name": "Catalina-utility-2", 433 | "stack": [ 434 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 435 | "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)", 436 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1674)", 437 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)", 438 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)", 439 | "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)", 440 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)", 441 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 442 | "org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)", 443 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 444 | ] 445 | } 446 | ], 447 | "threadCount": "2" 448 | }, 449 | { 450 | "container": "java.util.concurrent.ScheduledThreadPoolExecutor@6270db4d\/jdk.internal.vm.SharedThreadContainer@717b1d1d", 451 | "parent": "", 452 | "owner": null, 453 | "threads": [ 454 | { 455 | "tid": "71", 456 | "name": "RMI Scheduler(0)", 457 | "stack": [ 458 | "java.base\/jdk.internal.misc.Unsafe.park(Native Method)", 459 | "java.base\/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:269)", 460 | "java.base\/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1674)", 461 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1182)", 462 | "java.base\/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:899)", 463 | "java.base\/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1070)", 464 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)", 465 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 466 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 467 | ] 468 | } 469 | ], 470 | "threadCount": "1" 471 | }, 472 | { 473 | "container": "ForkJoinPool.commonPool\/jdk.internal.vm.SharedThreadContainer@496eeab8", 474 | "parent": "", 475 | "owner": null, 476 | "threads": [ 477 | ], 478 | "threadCount": "0" 479 | }, 480 | { 481 | "container": "java.util.concurrent.ThreadPoolExecutor@21961e8d\/jdk.internal.vm.SharedThreadContainer@3a553c28", 482 | "parent": "", 483 | "owner": null, 484 | "threads": [ 485 | { 486 | "tid": "70", 487 | "name": "RMI TCP Connection(5)-192.168.0.109", 488 | "stack": [ 489 | "java.base\/sun.nio.ch.Net.poll(Native Method)", 490 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:186)", 491 | "java.base\/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:275)", 492 | "java.base\/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:299)", 493 | "java.base\/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:340)", 494 | "java.base\/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:789)", 495 | "java.base\/java.net.Socket$SocketInputStream.read(Socket.java:1025)", 496 | "java.base\/java.io.BufferedInputStream.fill(BufferedInputStream.java:255)", 497 | "java.base\/java.io.BufferedInputStream.implRead(BufferedInputStream.java:289)", 498 | "java.base\/java.io.BufferedInputStream.read(BufferedInputStream.java:276)", 499 | "java.base\/java.io.FilterInputStream.read(FilterInputStream.java:71)", 500 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:580)", 501 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)", 502 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)", 503 | "java.base\/java.security.AccessController.doPrivileged(AccessController.java:399)", 504 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)", 505 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)", 506 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 507 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 508 | ] 509 | }, 510 | { 511 | "tid": "73", 512 | "name": "RMI TCP Connection(2)-192.168.0.109", 513 | "stack": [ 514 | "java.base\/java.lang.Object.wait0(Native Method)", 515 | "java.base\/java.lang.Object.wait(Object.java:366)", 516 | "java.management\/com.sun.jmx.remote.internal.ArrayNotificationBuffer.fetchNotifications(ArrayNotificationBuffer.java:449)", 517 | "java.management\/com.sun.jmx.remote.internal.ArrayNotificationBuffer$ShareBuffer.fetchNotifications(ArrayNotificationBuffer.java:227)", 518 | "java.management\/com.sun.jmx.remote.internal.ServerNotifForwarder.fetchNotifs(ServerNotifForwarder.java:273)", 519 | "java.management.rmi\/javax.management.remote.rmi.RMIConnectionImpl$4.run(RMIConnectionImpl.java:1271)", 520 | "java.management.rmi\/javax.management.remote.rmi.RMIConnectionImpl$4.run(RMIConnectionImpl.java:1269)", 521 | "java.management.rmi\/javax.management.remote.rmi.RMIConnectionImpl.fetchNotifications(RMIConnectionImpl.java:1275)", 522 | "java.base\/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", 523 | "java.base\/java.lang.reflect.Method.invoke(Method.java:578)", 524 | "java.rmi\/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:360)", 525 | "java.rmi\/sun.rmi.transport.Transport$1.run(Transport.java:200)", 526 | "java.rmi\/sun.rmi.transport.Transport$1.run(Transport.java:197)", 527 | "java.base\/java.security.AccessController.doPrivileged(AccessController.java:712)", 528 | "java.rmi\/sun.rmi.transport.Transport.serviceCall(Transport.java:196)", 529 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:598)", 530 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)", 531 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)", 532 | "java.base\/java.security.AccessController.doPrivileged(AccessController.java:399)", 533 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)", 534 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)", 535 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 536 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 537 | ] 538 | }, 539 | { 540 | "tid": "74", 541 | "name": "RMI TCP Connection(3)-192.168.0.109", 542 | "stack": [ 543 | "java.base\/sun.nio.ch.Net.poll(Native Method)", 544 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:186)", 545 | "java.base\/sun.nio.ch.NioSocketImpl.timedRead(NioSocketImpl.java:275)", 546 | "java.base\/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:299)", 547 | "java.base\/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:340)", 548 | "java.base\/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:789)", 549 | "java.base\/java.net.Socket$SocketInputStream.read(Socket.java:1025)", 550 | "java.base\/java.io.BufferedInputStream.fill(BufferedInputStream.java:255)", 551 | "java.base\/java.io.BufferedInputStream.implRead(BufferedInputStream.java:289)", 552 | "java.base\/java.io.BufferedInputStream.read(BufferedInputStream.java:276)", 553 | "java.base\/java.io.FilterInputStream.read(FilterInputStream.java:71)", 554 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:580)", 555 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:844)", 556 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:721)", 557 | "java.base\/java.security.AccessController.doPrivileged(AccessController.java:399)", 558 | "java.rmi\/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:720)", 559 | "java.base\/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)", 560 | "java.base\/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)", 561 | "java.base\/java.lang.Thread.run(Thread.java:1589)" 562 | ] 563 | } 564 | ], 565 | "threadCount": "3" 566 | }, 567 | { 568 | "container": "java.util.concurrent.ThreadPerTaskExecutor@634c5b98", 569 | "parent": "", 570 | "owner": null, 571 | "threads": [ 572 | { 573 | "tid": "83", 574 | "name": "", 575 | "stack": [ 576 | "java.base\/jdk.internal.vm.Continuation.yield(Continuation.java:357)", 577 | "java.base\/java.lang.VirtualThread.yieldContinuation(VirtualThread.java:370)", 578 | "java.base\/java.lang.VirtualThread.park(VirtualThread.java:499)", 579 | "java.base\/java.lang.System$2.parkVirtualThread(System.java:2606)", 580 | "java.base\/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)", 581 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)", 582 | "java.base\/jdk.internal.misc.ThreadFlock.awaitAll(ThreadFlock.java:315)", 583 | "jdk.incubator.concurrent\/jdk.incubator.concurrent.StructuredTaskScope.implJoin(StructuredTaskScope.java:461)", 584 | "jdk.incubator.concurrent\/jdk.incubator.concurrent.StructuredTaskScope.join(StructuredTaskScope.java:481)", 585 | "jdk.incubator.concurrent\/jdk.incubator.concurrent.StructuredTaskScope$ShutdownOnFailure.join(StructuredTaskScope.java:1059)", 586 | "victor.training.java.loom.StructuredConcurrency.get(StructuredConcurrency.java:34)", 587 | "java.base\/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)", 588 | "java.base\/java.lang.reflect.Method.invoke(Method.java:578)", 589 | "org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207)", 590 | "org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152)", 591 | "org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)", 592 | "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884)", 593 | "org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797)", 594 | "org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)", 595 | "org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1080)", 596 | "org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:973)", 597 | "org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1010)", 598 | "org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:902)", 599 | "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:705)", 600 | "org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:884)", 601 | "jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814)", 602 | "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223)", 603 | "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)", 604 | "org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)", 605 | "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)", 606 | "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)", 607 | "org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)", 608 | "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)", 609 | "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)", 610 | "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)", 611 | "org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)", 612 | "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)", 613 | "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)", 614 | "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)", 615 | "org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)", 616 | "org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)", 617 | "org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185)", 618 | "org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158)", 619 | "org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177)", 620 | "org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)", 621 | "org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542)", 622 | "org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119)", 623 | "org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)", 624 | "org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)", 625 | "org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)", 626 | "org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400)", 627 | "org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)", 628 | "org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859)", 629 | "org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734)", 630 | "org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)", 631 | "java.base\/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314)", 632 | "java.base\/java.lang.VirtualThread.run(VirtualThread.java:287)", 633 | "java.base\/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:174)", 634 | "java.base\/jdk.internal.vm.Continuation.enter0(Continuation.java:327)", 635 | "java.base\/jdk.internal.vm.Continuation.enter(Continuation.java:320)" 636 | ] 637 | } 638 | ], 639 | "threadCount": "1" 640 | }, 641 | { 642 | "container": "jdk.internal.misc.ThreadFlock@6373f7e2", 643 | "parent": "java.util.concurrent.ThreadPerTaskExecutor@634c5b98", 644 | "owner": "83", 645 | "threads": [ 646 | { 647 | "tid": "84", 648 | "name": "", 649 | "stack": [ 650 | "java.base\/jdk.internal.vm.Continuation.yield(Continuation.java:357)", 651 | "java.base\/java.lang.VirtualThread.yieldContinuation(VirtualThread.java:370)", 652 | "java.base\/java.lang.VirtualThread.park(VirtualThread.java:499)", 653 | "java.base\/java.lang.System$2.parkVirtualThread(System.java:2606)", 654 | "java.base\/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)", 655 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)", 656 | "java.base\/sun.nio.ch.Poller.poll2(Poller.java:139)", 657 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:102)", 658 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:87)", 659 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)", 660 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:196)", 661 | "java.base\/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:304)", 662 | "java.base\/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:340)", 663 | "java.base\/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:789)", 664 | "java.base\/java.net.Socket$SocketInputStream.read(Socket.java:1025)", 665 | "java.base\/java.io.BufferedInputStream.fill(BufferedInputStream.java:255)", 666 | "java.base\/java.io.BufferedInputStream.read1(BufferedInputStream.java:310)", 667 | "java.base\/java.io.BufferedInputStream.implRead(BufferedInputStream.java:382)", 668 | "java.base\/java.io.BufferedInputStream.read(BufferedInputStream.java:361)", 669 | "java.base\/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:827)", 670 | "java.base\/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:759)", 671 | "java.base\/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1684)", 672 | "java.base\/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1585)", 673 | "java.base\/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529)", 674 | "org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:81)", 675 | "org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)", 676 | "org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)", 677 | "org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:862)", 678 | "org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764)", 679 | "org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:378)", 680 | "victor.training.java.loom.StructuredConcurrency.fetchBeer(StructuredConcurrency.java:46)", 681 | "victor.training.java.loom.StructuredConcurrency.lambda$get$0(StructuredConcurrency.java:31)", 682 | "java.base\/java.util.concurrent.FutureTask.run(FutureTask.java:317)", 683 | "java.base\/java.lang.VirtualThread.run(VirtualThread.java:287)", 684 | "java.base\/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:174)", 685 | "java.base\/jdk.internal.vm.Continuation.enter0(Continuation.java:327)", 686 | "java.base\/jdk.internal.vm.Continuation.enter(Continuation.java:320)" 687 | ] 688 | }, 689 | { 690 | "tid": "85", 691 | "name": "", 692 | "stack": [ 693 | "java.base\/jdk.internal.vm.Continuation.yield(Continuation.java:357)", 694 | "java.base\/java.lang.VirtualThread.yieldContinuation(VirtualThread.java:370)", 695 | "java.base\/java.lang.VirtualThread.park(VirtualThread.java:499)", 696 | "java.base\/java.lang.System$2.parkVirtualThread(System.java:2606)", 697 | "java.base\/jdk.internal.misc.VirtualThreads.park(VirtualThreads.java:54)", 698 | "java.base\/java.util.concurrent.locks.LockSupport.park(LockSupport.java:369)", 699 | "java.base\/sun.nio.ch.Poller.poll2(Poller.java:139)", 700 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:102)", 701 | "java.base\/sun.nio.ch.Poller.poll(Poller.java:87)", 702 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:175)", 703 | "java.base\/sun.nio.ch.NioSocketImpl.park(NioSocketImpl.java:196)", 704 | "java.base\/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:304)", 705 | "java.base\/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:340)", 706 | "java.base\/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:789)", 707 | "java.base\/java.net.Socket$SocketInputStream.read(Socket.java:1025)", 708 | "java.base\/java.io.BufferedInputStream.fill(BufferedInputStream.java:255)", 709 | "java.base\/java.io.BufferedInputStream.read1(BufferedInputStream.java:310)", 710 | "java.base\/java.io.BufferedInputStream.implRead(BufferedInputStream.java:382)", 711 | "java.base\/java.io.BufferedInputStream.read(BufferedInputStream.java:361)", 712 | "java.base\/sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:827)", 713 | "java.base\/sun.net.www.http.HttpClient.parseHTTP(HttpClient.java:759)", 714 | "java.base\/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1684)", 715 | "java.base\/sun.net.www.protocol.http.HttpURLConnection.getInputStream(HttpURLConnection.java:1585)", 716 | "java.base\/java.net.HttpURLConnection.getResponseCode(HttpURLConnection.java:529)", 717 | "org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:81)", 718 | "org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)", 719 | "org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)", 720 | "org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:862)", 721 | "org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764)", 722 | "org.springframework.web.client.RestTemplate.getForObject(RestTemplate.java:378)", 723 | "victor.training.java.loom.StructuredConcurrency.fetchBeer(StructuredConcurrency.java:46)", 724 | "victor.training.java.loom.StructuredConcurrency.lambda$get$1(StructuredConcurrency.java:32)", 725 | "java.base\/java.util.concurrent.FutureTask.run(FutureTask.java:317)", 726 | "java.base\/java.lang.VirtualThread.run(VirtualThread.java:287)", 727 | "java.base\/java.lang.VirtualThread$VThreadContinuation.lambda$new$0(VirtualThread.java:174)", 728 | "java.base\/jdk.internal.vm.Continuation.enter0(Continuation.java:327)", 729 | "java.base\/jdk.internal.vm.Continuation.enter(Continuation.java:320)" 730 | ] 731 | } 732 | ], 733 | "threadCount": "2" 734 | }, 735 | { 736 | "container": "java.util.concurrent.ThreadPoolExecutor@383f4567\/jdk.internal.vm.SharedThreadContainer@4a763119", 737 | "parent": "", 738 | "owner": null, 739 | "threads": [ 740 | ], 741 | "threadCount": "0" 742 | }, 743 | { 744 | "container": "java.util.concurrent.ThreadPoolExecutor@3114d4ea\/jdk.internal.vm.SharedThreadContainer@74f339c5", 745 | "parent": "", 746 | "owner": null, 747 | "threads": [ 748 | ], 749 | "threadCount": "0" 750 | } 751 | ] 752 | } 753 | } 754 | --------------------------------------------------------------------------------