Phase 4-এ যা মিস করেছিলে
Phase 4-এ তুমি CompletableFuture দিয়ে একাধিক অ্যাসিঙ্ক্রোনাস কাজ কম্পোজ করা শিখেছিলে — thenCompose, allOf, anyOf। কিন্তু লক্ষ্য করলে দেখবে একটা সমস্যা লুকিয়ে ছিল: যদি দুইটা সমান্তরাল কলের একটা exception ছোঁড়ে, অন্যটা কি থামে? allOf() দিয়ে করা কলে উত্তর হলো — না, এটা চলতেই থাকে, এমনকি প্রথমটা ফেইল করার পরেও। ব্যর্থ টাস্কের রেজাল্ট কেউ ব্যবহার না করলেও থ্রেড কাজ করেই যায়, রিসোর্স খরচ করেই যায় — এটাকে বলে unstructured concurrency: চাইল্ড টাস্কগুলোর lifetime তাদের প্যারেন্ট স্কোপের সাথে বাঁধা নেই।
স্ট্রাকচার্ড কনকারেন্সির মূল আইডিয়া
স্ট্রাকচার্ড কনকারেন্সি (Java 21+-এ প্রিভিউ হিসেবে উপলব্ধ) একটা সহজ নিয়ম চাপায়: যদি একটা কাজ সাবটাস্ক শুরু করে, সেই সাবটাস্কগুলো তার নিজের স্কোপের ভেতরেই শেষ হবে — ঠিক যেমন একটা মেথডের ভেতরে খোলা try-with-resources ব্লকের বাইরে কোনো রিসোর্স "পালাতে" পারে না। এটা try-with-resources-এর কনকারেন্সি ভার্সন বলা যায়।
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Supplier<String> user = scope.fork(() -> fetchUser(userId));
Supplier<String> orders = scope.fork(() -> fetchOrders(userId));
scope.join(); // দুটোই শেষ হওয়া পর্যন্ত অপেক্ষা করে
scope.throwIfFailed(); // কোনোটা ব্যর্থ হলে exception ছোঁড়ে
return new Profile(user.get(), orders.get());
} // স্কোপ বন্ধ হলে সব চাইল্ড টাস্ক নিশ্চিতভাবে শেষ/বাতিল হয়ে যায়
ShutdownOnFailure: একজন ব্যর্থ হলে সবাই থামে
ShutdownOnFailure পলিসি ব্যবহার করলে, যদি fetchUser বা fetchOrders যেকোনো একটা exception ছোঁড়ে, স্কোপ সাথে সাথে অন্য সব চলমান সাবটাস্ককে cancel করার সিগন্যাল পাঠায় (তাদের থ্রেড interrupt হয়)। এটা Phase 4-এ CompletableFuture.allOf()-এ যা পাওয়া যেত না তার ঠিক বিপরীত — সেখানে একটা ব্যর্থ হলেও বাকিগুলো চলতেই থাকত, রিসোর্স নষ্ট করত।
ShutdownOnSuccess: প্রথমজন জিতলেই যথেষ্ট
উল্টো পরিস্থিতির জন্য — যেমন একই ডাটা কয়েকটা রিপ্লিকা সার্ভার থেকে সমান্তরালে চাওয়া, প্রথম যেটা সাড়া দেয় সেটাই যথেষ্ট — ShutdownOnSuccess ব্যবহার করা যায়:
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
scope.fork(() -> fetchFromReplica("replica-1"));
scope.fork(() -> fetchFromReplica("replica-2"));
scope.fork(() -> fetchFromReplica("replica-3"));
scope.join();
String fastestResult = scope.result(); // যেটা আগে এলো
return fastestResult;
} // বাকি দুটো সাবটাস্ক স্বয়ংক্রিয়ভাবে cancel হয়ে যায়
এটা ঠিক Phase 4-এ CompletableFuture.anyOf()-এর মতোই আচরণ করে, কিন্তু একটা বড় পার্থক্য সহ: anyOf() পরাজিত future-গুলোকে চলতে দেয়, ShutdownOnSuccess সেগুলোকে সক্রিয়ভাবে বাতিল করে।
কেন এটা গুরুত্বপূর্ণ: স্ট্যাক ট্রেস আর ক্যান্সেলেশন
CompletableFuture চেইনের একটা পরিচিত সমস্যা — exception হলে স্ট্যাক ট্রেস দেখায় .then() এর ভেতরের লাইন, কিন্তু আসল কলার কোথায় ছিল সেটা হারিয়ে যায়। স্ট্রাকচার্ড কনকারেন্সিতে যেহেতু সাবটাস্কগুলোর lifetime প্যারেন্ট স্কোপের সাথে সরাসরি বাঁধা (একটা try ব্লক), ডিবাগার আর স্ট্যাক ট্রেস দুটোই স্বাভাবিক, সরল ব্লকিং কোডের মতোই কাজ করে — একটা গাছের কাঠামোর মতো (parent-child), যা থ্রেড ডাম্পেও স্পষ্টভাবে দেখা যায়।
একটা গুরুত্বপূর্ণ নোট
Java 21-এ এই API একটা preview feature হিসেবে আসে (--enable-preview ফ্ল্যাগ দরকার), আর ভবিষ্যৎ ভার্সনে ধীরে ধীরে স্থিতিশীল হয়েছে। প্রোডাকশনে ব্যবহারের আগে তোমার JDK ভার্সনে এর স্ট্যাটাস যাচাই করে নাও।