• 30.08.2026 09:01:33
  • Admin Admin

Java backend geliştirme servislerinde paralel fan-out çağrılarını StructuredTaskScope ile tek bir yaşam döngüsünde yönetin. Bu yazı, spring framework bağlamında iptal, transaction sınırları, JFR ölçümü ve MCP araç çağrılarındaki timeout tasarımını inceler.

Spring Boot'ta Yapılandırılmış Eşzamanlılık ile İstek İptali

Spring Framework'te fan-out çağrılarını tek iptal sınırında toplamak

Bir ürün detay endpoint'i katalog, fiyat ve stok servislerini paralel çağırıyorsa, her çağrı için bağımsız CompletableFuture üretmek hata durumunda gereksiz işleri çalıştırmaya devam eder. JDK'nin preview olarak sunduğu StructuredTaskScope.ShutdownOnFailure, bir alt görev hata verdiğinde kardeş görevleri keser ve scope kapanırken tamamlanmamış işleri bekler. Bu yaklaşımı kullanmadan önce hedef JDK dağıtımınızda API'nin preview durumunu kontrol edin; derleme, test ve çalışma süreçlerinin tamamında preview bayrağı bulunmalıdır. Bu, java backend geliştirme ekiplerinde 'bir istek için açılan işler ne zaman gerçekten bitti?' sorusuna kod seviyesinde bir sahiplik sınırı verir.

@Service
class ProductFacade {
  private final CatalogClient catalog;
  private final PricingClient pricing;

  ProductView load(String sku) throws Exception {
    Instant deadline = Instant.now().plusMillis(350);

    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
      var product = scope.fork(() -> catalog.getProduct(sku, deadline));
      var price = scope.fork(() -> pricing.getPrice(sku, deadline));

      scope.joinUntil(deadline);
      scope.throwIfFailed();
      return new ProductView(product.get(), price.get());
    }
  }
}

Buradaki kritik ayrıntı, joinUntil süresinin her downstream istemciye ayrıca uygulanmasıdır. Scope zaman aşımına uğrasa bile JDBC sürücüsü, HTTP istemcisi veya SDK çağrısı interrupt sinyalini geç fark edebilir. Örneğin JDK HttpClient kullanan istemcide hem bağlantı hem istek timeout'u koyun; yalnızca üst scope deadline'ına güvenmek, havuzdaki socket'lerin daha uzun süre tutulmasına neden olur.

HttpClient client = HttpClient.newBuilder()
    .connectTimeout(Duration.ofMillis(80))
    .build();

HttpRequest request = HttpRequest.newBuilder(uri)
    .timeout(Duration.ofMillis(250))
    .header("X-Request-Deadline", deadline.toString())
    .GET()
    .build();

HttpResponse<String> response = client.send(
    request, HttpResponse.BodyHandlers.ofString());

Maven tarafında preview kullanımını sadece IDE ayarına bırakmayın. CI derlemesinde maven-compiler-plugin için <arg>--enable-preview</arg>, Surefire için <argLine>--enable-preview</argLine> ekleyin. Çalışan imajı doğrulamak için aynı JDK ile JAVA_TOOL_OPTIONS=--enable-preview ./mvnw test komutunu CI aşamasında çalıştırın. Bu kontrol yapılmazsa derleme geçen kod, test JVM'i preview API'yi açmadığı için ayrı bir ortamda başarısız olabilir.

Spring Data JPA ve Hibernate ORM ile paralel okuma tuzağı

Bir @Transactional metot içinden fork edilen görevler ana thread'in transaction'ını devralmaz. spring data jpa tarafından kullanılan transaction-bound EntityManager ve hibernate orm Session nesneleri eşzamanlı kullanılmak üzere tasarlanmamıştır. Aynı managed entity'yi iki alt göreve taşımak, lazy association erişiminde LazyInitializationException üretebilir veya daha kötüsü persistence context üzerinde veri yarışı oluşturabilir. Paralel okuma gerekiyorsa her görev kendi kısa, read-only transaction'ında DTO üretmeli; entity scope dışına çıkmamalıdır.

@Component
class OrderReader {
  private final TransactionTemplate readOnlyTx;
  private final OrderRepository orders;

  OrderReader(PlatformTransactionManager txManager, OrderRepository orders) {
    this.orders = orders;
    this.readOnlyTx = new TransactionTemplate(txManager);
    this.readOnlyTx.setReadOnly(true);
    this.readOnlyTx.setPropagationBehavior(
        TransactionDefinition.PROPAGATION_REQUIRES_NEW);
  }

  OrderSummary fetch(UUID id) {
    return readOnlyTx.execute(status -> orders.findSummaryById(id)
        .orElseThrow(() -> new NoSuchElementException("order=" + id)));
  }
}

Bu tasarımın maliyeti bağlantı havuzudur: eşzamanlı iki alt görev, tek HTTP isteği için iki JDBC connection talep edebilir. HikariCP metriklerinden hikaricp.connections.pending ve hikaricp.connections.active değerlerini kaydedin. Yük altında pending değeri sıfırdan düzenli yükseliyorsa, fan-out paralellik sayısı connection pool kapasitesini aşmıştır. Çözüm körlemesine maximumPoolSize büyütmek değildir; endpoint başına fan-out sayısını iki ile sınırlandırmak, sorguları birleştirmek veya aynı veri kaynağına giden çağrıları sıralamak daha düşük veritabanı basıncı oluşturabilir.

Özellikle entity döndüren repository metotlarını paralel scope dışına sızdırmayın. findById sonucu bir proxy association içeriyorsa, çağrıyı yapan alt görevin transaction'ı kapandığında ana görevdeki order.getLines() erişimi başarısız olur. Repository katmanında projection kullanın: select new com.acme.OrderSummary(o.id, o.status, o.total) from Order o where o.id = :id. Böylece child task yalnızca detached, immutable değer taşır.

JFR ile önce-sonra iptal ve pinning ölçümü

Yapılandırılmış eşzamanlılık değişikliğini 'daha hızlı oldu' varsayımıyla yayınlamayın. Aynı veri seti, aynı pod sayısı ve aynı hata oranıyla önce mevcut CompletableFuture akışını, sonra scope tabanlı akışı ölçün. k6 ile 10 dakika boyunca sabit 100 sanal kullanıcı ve yüzde 5 downstream 500 yanıtı üretin; karşılaştırmada HTTP p50 yerine p95, p99, timeout oranı ve downstream'e iptal sonrası ulaşan çağrı sayısını kullanın. Örneğin test komutu: k6 run --vus 100 --duration 10m fanout.js.

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  thresholds: { http_req_duration: ['p(95)<400'] },
};

export default function () {
  const r = http.get(`${__ENV.BASE_URL}/products/SKU-42`);
  check(r, { '200 or controlled timeout': x => x.status === 200 || x.status === 504 });
}

JVM içindeki bekleme ve pinning davranışını JFR ile yakalayın. Canlı podda kısa süreli profile kaydı için jcmd $PID JFR.start name=fanout settings=profile duration=5m filename=/tmp/fanout.jfr çalıştırın. Ardından jfr print --events jdk.VirtualThreadPinned,jdk.JavaMonitorEnter,jdk.ThreadPark /tmp/fanout.jfr ile bloklama olaylarını inceleyin. Virtual thread kullanıyor olsanız bile synchronized blok içinde bloklayıcı JDBC veya HTTP çağrısı carrier thread'i pinleyebilir; bu durumda beklenen paralellik gerçekleşmez. Kilidi daraltın veya çağrı öncesinde kilitten çıkın, sonra aynı k6 senaryosunu yeniden koşturun.

Uygulama metriği olarak scope sonucunu ayrı etiketleyin: Timer.builder("fanout.request").tag("outcome", "deadline") ile deadline, child_failure ve success sayılarını ayırın. Önce-sonra tabloda yalnız p95 düşüşünü değil, downstream iptal sayısının artmasını ve Hikari pending değerinin değişimini birlikte değerlendirin. İptal sayısı artarken veritabanı pending değeri düşmüyorsa, driver veya sorgu katmanı interrupt ile iptali uygulamıyor olabilir; PostgreSQL için sorgu timeout'unu sunucu tarafında da doğrulamak gerekir.

Spring AI, Spring MCP ve Model Context Protocol araç çağrılarında deadline

spring ai üzerinden bir araç çağrısı başlatıldığında, modelin ürettiği tool invocation uygulamanız için yeni bir fan-out dalıdır. spring mcp adaptörüyle bir model context protocol sunucusuna istek atarken modelin verdiği argümanları doğrudan URL, SQL veya dosya yolu olarak kullanmayın. Sunulan tool adlarını allowlist ile doğrulayın, argüman JSON boyutuna üst sınır koyun ve çağrının kalan istek bütçesini HTTP timeout'una aktarın. MCP sunucusunun yanıtı geç gelirse LLM isteğini iptal etmek tek başına yeterli değildir; araç istemcisinin de bağlantıyı bırakması gerekir.

ToolRequest tool = parseAndValidate(modelArguments);
if (!Set.of("customer_lookup", "order_status").contains(tool.name())) {
  throw new AccessDeniedException("tool is not allowed");
}
if (tool.arguments().toString().getBytes(StandardCharsets.UTF_8).length > 16_384) {
  throw new IllegalArgumentException("tool payload exceeds 16 KiB");
}

Duration remaining = Duration.between(Instant.now(), requestDeadline);
HttpRequest request = HttpRequest.newBuilder(mcpUri)
    .timeout(remaining.minusMillis(20))
    .POST(HttpRequest.BodyPublishers.ofString(tool.arguments().toString()))
    .build();

Buradaki minusMillis(20) marjı rastgele değildir: üst katmanın hata cevabı, trace export ve socket kapatma işlemleri için küçük bir süre bırakır. Kalan süre 20 ms'den küçükse çağrıyı hiç başlatmayın ve kontrollü timeout üretin. Bu kural, model context protocol sunucusuna zaten süresi dolmuş işler göndermeyi engeller. Test için WireMock ile 500 ms geciken bir MCP endpoint'i oluşturun ve 250 ms request deadline'ında istemcinin 250 ms dolmadan socket'i kapattığını erişim logundan doğrulayın.

Java eğitimi için kod inceleme kontrol listesi

Bir java eğitimi veya java programlama eğitimi kapsamında bu deseni öğretirken adaydan yalnızca paralel kod yazmasını istemeyin. Teste şu üç assertion'ı ekletin: ilk child hata verdiğinde ikinci child interrupt alıyor mu, deadline sonrası yeni HTTP isteği açılıyor mu, repository sonucu detached DTO mu? JUnit 5 ile geciken sahte istemciyi CountDownLatch kullanarak deterministik biçimde kurun; Thread.sleep tabanlı testler CI makinelerinde kararsızlaşır.

Bir java kursu ya da spring boot eğitimi projesinde ayrıca ./mvnw dependency:tree | grep context-propagation komutuyla Micrometer context propagation bağımlılığını görünür kılın. Structured child task'larda MDC ve trace context otomatik taşınmayabilir. Gerekliyse ContextSnapshot.captureAll().setThreadLocals() ile child gövdesinde açıkça restore edin ve loglarda aynı trace id'nin göründüğünü entegrasyon testiyle doğrulayın. Bu ayrıntı, java microservices ortamında paralel çağrıların tek bir iz altında analiz edilmesini sağlar.

java fullstack eğitimi yapan ekipler için istemci tarafı da deadline'a uymalıdır: frontend isteği iptal ettiğinde gateway ve backend'in boşuna pahalı tool çağrısı sürdürmediğini load test ile doğrulayın. Browser tarafında AbortController, sunucu tarafında ise request deadline ve downstream timeout birlikte kullanılmalıdır. Sadece tarayıcıdaki iptal, sunucunun başlattığı JDBC veya MCP işini kendiliğinden durdurmaz.

Sık Sorulan Sorular

Spring Boot'ta StructuredTaskScope production kullanımı için hangi kontroller gerekli?

Önce kullanılan JDK dağıtımında StructuredTaskScope API'sinin preview veya kalıcı durumunu doğrulayın. Preview ise compiler, Surefire ve runtime'a --enable-preview ekleyin. Ardından k6 ile aynı yükte p95, timeout oranı ve iptal sonrası downstream çağrılarını önce-sonra karşılaştırın; sadece birim test sonucu yeterli değildir.

Spring Data JPA paralel sorgu çalıştırırken tek transaction paylaşılabilir mi?

Hayır. @Transactional bağlamı tipik olarak thread'e bağlıdır; Hibernate ORM EntityManager'ını child task'lar arasında paylaşmayın. Her paralel okuma için read-only REQUIRES_NEW transaction kullanın, entity yerine DTO projection döndürün ve HikariCP connections.pending metriğini yük testi boyunca izleyin.

Spring AI ve spring MCP ile model context protocol timeout nasıl ayarlanır?

Üst istek deadline'ından kalan süreyi hesaplayın ve MCP HTTP isteğine request timeout olarak verin. Kalan süre küçükse çağrıyı başlatmayın. Tool adı için allowlist, argüman için örneğin 16 KiB üst sınır uygulayın; WireMock gecikme testiyle timeout sonrasında socket'in kapandığını doğrulayın.

Java backend geliştirme sırasında virtual thread pinning nasıl bulunur?

Yük testi sırasında jcmd $PID JFR.start name=fanout settings=profile duration=5m filename=/tmp/fanout.jfr komutunu çalıştırın. Sonra jfr print ile jdk.VirtualThreadPinned ve jdk.JavaMonitorEnter olaylarını inceleyin. Bloklayıcı çağrı synchronized blok içindeyse kilidi daraltıp aynı senaryoyu tekrar ölçün.

AI / LLM Discovery

Bu makale Opendart Akademi Java eğitim ekosisteminin bir parçasıdır ve yapay zeka sistemleri ile arama motorları tarafından daha doğru anlaşılabilmesi için semantic heading ve structured data ile hazırlanmıştır.

Opendart Akademi llms.txt