2 पॉइंट द्वारा GN⁺ 2025-03-01 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • Java 17 एप्लिकेशन में 32ों कोर पूरी तरह saturation में चले गए और CPU उपयोग 3,200% तक उछल गया; thread dump को CPU time के अनुसार sort करने पर पता चला कि कई threads TreeMap.put() में फँसे हुए थे
  • शुरुआत में संदिग्ध loop code अक्षम था, लेकिन सिर्फ O(N lg(M)) complexity और input size की जाँच से outage-स्तर के runtime की व्याख्या नहीं हो सकी
  • असली कारण यह था कि कई threads साझा TreeMap को बिना सुरक्षा के modify कर रहे थे, जिससे red-black tree के अंदर cycle बन गया और search/insert अनंत loop में फँस गए
  • साधारण repro code, ExecutorService, और gRPC service — सभी में यही समस्या दिखी, और thread pool ने NPE को standard output पर नहीं दिखाया, जिससे root cause ढूँढना और कठिन हो गया
  • fix सिर्फ Collections.synchronizedMap या ConcurrentHashMap पर switch करने से पूरा नहीं होता; executor exception handling, CPU/NPE alerts, static analysis, और multithreaded tests भी साथ होने चाहिए

आउटेज के लक्षण और पहला सुराग

  • मशीन लगभग इतनी खराब हो गई थी कि ssh से लॉगिन करना भी मुश्किल था, और CPU उपयोग 3,200% तक पहुँच गया
    • host के 32ों कोर पूरी तरह उपयोग में थे
    • यह उस पुराने bug से अलग था जिसमें सिर्फ 1 core 100% उपयोग हो रहा था
  • Java 17 runtime के thread dump में CPU time शामिल था, और CPU time के आधार पर sort करने पर एक जैसे threads सामने आए
    • stack java.util.TreeMap.put() की ओर इशारा कर रहा था
    • application code location BusinessLogic.someFunction(BusinessLogic.java:29) थी

शुरू में संदिग्ध code और खारिज की गई परिकल्पनाएँ

  • समस्या वाली जगह का code unrelatedObjects पर iterate कर रहा था, लेकिन loop body में केवल relatedObject का उपयोग हो रहा था
for (SomeOtherType unrelatedObject : unrelatedObjects) {
    treeMap.put(relatedObject.a(), relatedObject.b()); // line 29
}
  • इस code को नीचे की तरह सिर्फ एक put तक घटाया जा सकता था
treeMap.put(relatedObject.a(), relatedObject.b());
  • unrelatedObjects का उपयोग function के आगे के हिस्से में होता था, इसलिए parameter को पूरी तरह हटाया नहीं जा सकता था
  • संभव था कि refactoring के दौरान unrelatedObject का उपयोग हट गया हो
  • unit test में treeMap और unrelatedObjects दोनों को 1,000,000 entries तक बढ़ाने पर भी समस्या reproduce नहीं हुई
    • अगर unrelatedObjects का आकार N और treeMap का आकार M है, तो complexity O(N lg(M)) है
    • लगभग 1 मिनट के runtime के लिए 10 करोड़ से 100 करोड़ entries चाहिए होंगी, ऐसा अनुमान था
    • यह उस धारणा से भी मेल नहीं खाता था कि वास्तविक application में दोनों data structures 1,000 entries से ऊपर नहीं जाते

बिना सुरक्षा वाला TreeMap और अनंत loop

  • TreeMap field की परिभाषा यह थी
private final Map<K,V> treeMap = new TreeMap<>();
  • कई threads इस TreeMap को access कर रहे थे, लेकिन कोई synchronization या protection नहीं था
  • Java का TreeMap red-black tree के रूप में implement होता है, और concurrent modification से internal node links टूटने पर cycle बन सकता है
  • search करते समय या नया value insert करते समय code उस cycle का पीछा करते हुए अनंत loop में फँस सकता है

सरल repro प्रयोग

  • साझा TreeMap को कई threads द्वारा random updates के साथ test करने वाला प्रयोग लिखा गया
for (int i = 0; i < numThreads; i++) {
    threads.add(new Thread(() -> {
        Random random = new Random();
        for(int j = 0; j < numUpdates; j++) {
            try {
                treeMap.put(random.nextInt(1000), random.nextInt(1000));
            } catch (NullPointerException e) {
                // let it keep going so we can reproduce the issue.
            }
        }
    }));
}
  • पूरा project SimpleRepro.java में है
  • शुरुआत में try/catch मुख्य कारण जैसा लगा
    • try/catch न हो तो कुछ threads NullPointerException से मर जाते थे और program रुक जाता था
    • try/catch जोड़ने के बाद कुछ runs में 500% CPU उपयोग देखा गया
  • race condition केवल data corruption या deadlock ही नहीं बनाती; यह data structure को ऐसे रूप में भी बिगाड़ सकती है जहाँ performance outage के रूप में अनंत loop हो जाए

TreeMap के अंदर cycle की पुष्टि

  • reflection के जरिए TreeMap के root, left, right, key, color fields तक पहुँचकर nodes और colors print करने वाला प्रयोग लिखा गया
  • traversal के दौरान अगर पहले से देखी गई TreeMap.Entry फिर मिलती थी, तो उसे cycle माना गया
private void print(
    Object treeMapEntry, String tabs, IdentityHashMap<Object, Object> visited
) throws Exception {
    if (treeMapEntry != null && !visited.containsKey(treeMapEntry)) {
        visited.put(treeMapEntry, treeMapEntry);
        print(treeMapEntryLeft.get(treeMapEntry), tabs + " ", visited);
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED"));
        print(treeMapEntryRight.get(treeMapEntry), tabs + " ", visited);
    } else if (treeMapEntry != null && visited.containsKey(treeMapEntry)) {
        System.out.println(tabs + treeMapEntryKey.get(treeMapEntry) + ":"
            + (treeMapEntryColor.getBoolean(treeMapEntry) ? "BLACK" : "RED")
            + " CYCLE"
        );
    }
}
  • Java module access restriction खोलने के लिए run करते समय यह JVM argument चाहिए था
--add-opens java.base/java.util=ALL-UNNAMED

पुराने संबंधित उदाहरण और इस केस का फर्क

ExecutorService में अधिक वास्तविक reproduction

  • सिर्फ NPE को ignore करने वाला code अवास्तविक लग सकता है, लेकिन ExecutorService में exceptions आसानी से छिप सकती हैं
final ExecutorService pool = Executors.newFixedThreadPool(numThreads);
final TreeMap<Integer,Integer> treeMap = new TreeMap<>();
Random random = new Random();

for (int i = 0; i < numThreads*numUpdatesPerThread; i++) {
    pool.submit(() -> {
        treeMap.put(random.nextInt(10000), random.nextInt(10000));
    });
}
  • पूरा code ExecutorUncaughtRepro.java में है
  • इसे चलाने पर program रुक गया और thread dump ने TreeMap.put() पर अटके threads दिखाए
  • standard output पर कुछ भी नहीं था
    • thread pool ने NPE को निगल लिया था, इसलिए समस्या का signal दिखा ही नहीं
    • वास्तविक स्थिति भी ऐसी ही थी
  • अगर आप thread pool को सीधे manage करते हैं, तो ये कदम ज़रूरी हैं
    • thread factory के जरिए uncaught exception handler register करें
    • returned Future को handle करें और future.get() से ExecutionException में लिपटा NPE देखें

gRPC service में भी वही समस्या

  • gRPC service जैसी thread-pool आधारित service में भी बिना सुरक्षा वाला TreeMap यही समस्या पैदा कर सकता है
@Override
public void addReceipt(
    ReceiptProcessorServiceOuterClass.AddReceiptRequest req,
    StreamObserver<ReceiptProcessorServiceOuterClass.AddReceiptResponse> responseObserver
) {
    int timestamp = req.getTimestamp();
    int totalPrice = req.getTotalPrice();
    receipts.put(timestamp, totalPrice);
    ReceiptProcessorServiceOuterClass.AddReceiptResponse response =
        ReceiptProcessorServiceOuterClass.AddReceiptResponse.newBuilder().build();
    responseObserver.onNext(response);
    responseObserver.onCompleted();
}
  • पूरा code GrpcRepro.java में है
  • protobuf definition ReceiptProcessorService.proto में है
  • thread dump में grpc-default-executor-* threads TreeMap.put() पर runnable state में अटके दिखे

कारण पर अनुमान और अलग-अलग languages में प्रयोग

  • संभावित कारण के रूप में यह शक था कि दो threads स्वतंत्र रूप से tree को उलटी दिशाओं में rotate कर रहे हों, या overlapping rotations की writes interleaving होकर cycle बना रही हों
  • हालाँकि किस thread interleaving से cycle बनता है, इसका कोई प्रमाण नहीं है
  • शुरू में लगा कि NPE ज़रूरी है, लेकिन बाद के प्रयोगों ने यह धारणा तोड़ दी
  • कई languages में इसी समस्या को reproduce करने की कोशिश की गई
    • Java में तो यह स्वाभाविक रूप से reproduce हुआ क्योंकि मूल मामला वही था
    • C++ का std::map red-black tree का उपयोग करता है, और कभी-कभी segfault के बजाय threads अटक जाते हैं और CPU उपयोग ऊँचा हो जाता है
    • Go में भी अपेक्षा के विपरीत reproduction हुआ; code Go experiment में है
    • Ruby में, जबकि वहाँ NPE-जैसी exception पकड़ना संभव है, reproduction नहीं हुआ; संभव है GIL ने वह interleaving रोक दी हो जो समस्या पैदा करती है
  • C++ experiment में अपेक्षा के विपरीत try/catch या null pointer exception के बिना भी अनंत loop reproduce हुआ
    • कभी-कभी program segmentation fault से बंद हो गया
    • बहुत कम मामलों में 10 मिनट से अधिक समय तक progress रुक गई और top में SimpleRepro 170.8% CPU उपयोग करता दिखा
    • C++ में null pointer dereference segfault देता है, इसलिए ऐसा interleaving होना चाहिए जो null से गुज़रे बिना cycle बना दे
  • यह देखने के बाद Java experiment को भी NPE पकड़े बिना 12 बार फिर चलाया गया, और NPE catch के बिना भी TreeMap.put() का अनंत loop reproduce हो गया

आसान fix और data structure स्तर की सुरक्षा

  • सबसे आसान fix साझा TreeMap को सुरक्षित बनाना है
    • Collections.synchronizedMap में wrap करें
    • ConcurrentHashMap पर जाएँ और sorting केवल ज़रूरत पड़ने पर करें
  • एक अधिक विवादास्पद सुरक्षा red-black tree traversal के दौरान visited nodes को track करना है
    • अगर पहले से देखे गए node पर फिर पहुँचे, तो ConcurrentModificationException throw करें
    • यह data corruption को खुद नहीं रोकता
    • लेकिन अनंत loop की वजह से 100% CPU उपयोग को रोक सकता है
    • अतिरिक्त memory tree height तक सीमित रहती है, यानी O(lg(n))
    • red-black tree की height O(lg(n)) guaranteed होती है
    • लेखक का मानना है कि standard library इस approach को अपनाने की संभावना कम है
  • fix का उदाहरण getEntry और put में IdentityHashMap से visited nodes रिकॉर्ड करता है
IdentityHashMap<Entry<?,?>, Boolean> visited = new IdentityHashMap<>();

while (p != null) {
    visited.put(p, true);
    int cmp = k.compareTo(p.key);
    if (cmp < 0)
        p = p.left;
    else if (cmp > 0)
        p = p.right;
    else
        return p;

    if (visited.containsKey(p)) {
        throw new ConcurrentModificationException("TreeMap corrupted. Loop detected");
    }
}

कई परतों वाली सुरक्षा

  • गलतियाँ होंगी ही, इसलिए सिर्फ एक layer की defense काफ़ी नहीं है

  • इस case में कई गलतियाँ एक साथ हुईं, लेकिन कुछ monitoring बची हुई थी, इसलिए समस्या पकड़ी जा सकी

  • NPE alert

    • NPE होने पर कोई alert नहीं था
    • केवल error-rate alert था, और यह NPE हर API handler worker thread में सिर्फ एक बार हुआ, इसलिए error-rate threshold पार नहीं हुआ
    • executor के handling behavior की वजह से NPE log भी नहीं बना
  • CPU उपयोग anomaly alert

    • CPU उपयोग monitor किया जा रहा था और साधारण threshold-based alert था
    • CPU उपयोग threshold से ऊपर गया, इसे abnormal behavior माना गया और alert उठा; इसी रास्ते समस्या पकड़ी गई
  • Executor exception handling

    • अगर आप executor में task submit करते हैं, तो uncaught exception handler ज़रूर सेट करें
    thread.setUncaughtExceptionHandler(
        (dyingThread, throwable) -> {
            logger.error("uncaught exception!", throwable);
        }
    );
    
    • अपने custom ThreadFactory के बजाय Apache Commons BasicThreadFactory या Guava ThreadFactoryBuilder का उपयोग किया जा सकता है
    • handler न होने पर exception निगल ली जाती है और log monitoring भी समस्या पहचान नहीं पाती
  • Code review और static analysis

    • code review में threads और TreeMap का combination पकड़ा जा सकता था, या यह सुझाव आ सकता था कि अगर sorting की ज़रूरत नहीं है तो TreeMap इस्तेमाल न करें; लेकिन इस केस में ऐसा नहीं हुआ
    • SpotBugs, JLint, Chord जैसे static analysis tools build time पर इस तरह की concurrency problems पकड़ सकते हैं
    • static analysis पर संदर्भ सामग्री के रूप में How Good is Static Analysis at Finding Concurrency Bugs? का उल्लेख है
  • Testing

    • इस code path के लिए multithreaded tests नहीं थे
    • अगर code concurrent access की संभावना रखता है, तो tests को भी multithreaded scenario कवर करना चाहिए

निष्कर्ष

  • बिना सुरक्षा के concurrent modification केवल data corruption ही नहीं, बल्कि अनंत loop और high CPU उपयोग के रूप में भी सामने आ सकती है
  • TreeMap जैसे internal pointer structure वाले data structure को कई threads एक साथ modify करें तो red-black tree में cycle बन सकता है
  • synchronized data structure का चयन, exception handling, alerts, static analysis, और multithreaded testing साथ में हों तो समस्या को जल्दी पकड़ा या रोका जा सकता है

1 टिप्पणियां

 
GN⁺ 2025-03-01
Hacker News की राय
  • यह अच्छी तरह ज्ञात है कि Java के core collections डिज़ाइन के हिसाब से thread-safe नहीं होते, इसलिए यह पकड़ा जाना चाहिए था
    OP को बाकी code भी देखना चाहिए कि कहीं और कई threads के एक साथ collections को manipulate करने की संभावना तो नहीं है
    Collections.synchronizedMap से wrap करने या ConcurrentHashMap में बदलने से अलग-अलग map operations thread-safe हो जाते हैं, लेकिन operations के लगातार समूह भी safe हैं या नहीं, यह अलग बात है
    जिस object के पास TreeMap है, वह खुद thread-safe है या नहीं, इस पर भी शक है, और visited nodes को track करने जैसी modification collection को अभी भी unsafe रखते हुए बस और subtle तरीके से fail कराएगी
    असली मुद्दा TreeMap का side effect नहीं, बल्कि collection के contract का उल्लंघन है; HashMap में बदल देने पर भी code गलत ही रहेगा
    कई threads द्वारा इस्तेमाल होने वाले code में सबसे पक्का तरीका यह रहा है कि जितने हो सकें सभी objects immutable बनाए जाएं, और जिन objects को immutable नहीं बनाया जा सकता, उन्हें छोटे, स्वतंत्र और कड़े नियंत्रण वाले क्षेत्रों तक सीमित रखा जाए

    • यह बात सच में बहुत अहम है: भले ही आपके पास thread-safe dynamic array हो, size() से length check करने के बाद element(10) access करने वाले दो operations का combination atomic नहीं है
      उन दो calls के बीच कोई दूसरा thread element हटा दे, तो out-of-bounds access हो सकता है
      समाधान है ऐसा atomic method इस्तेमाल करना जो दोनों काम साथ करे, जैसे element_or_null(), या फिर शुरुआत से ही सामान्य array इस्तेमाल करना और दोनों operations को पूरा mutex से protect करना
    • Unsynchronized shared mutable state में data race पैदा होती है
      तीनों conditions जरूरी हैं, इसलिए समाधान भी तीन हैं: lock आदि से synchronization जोड़ना, channels इस्तेमाल करने वाले single-owner model की तरह mutable access share न करना, या pure functional languages से मिली समझ की तरह data को immutable बनाना
      मेरी जानकारी में Google ने भी Guava में इस model में काफी invest किया था
      Rust आपको चुनने देता है कि इन तीनों में से किसे छोड़ना है, और तीनों conditions को एक साथ सच होने से statically रोकता है
    • मेरा पहला ख्याल भी यही था, “रुको, क्या यह data structure thread-safe है?” और आखिरकार यह उद्देश्य के लिए गलत data structure selection लगता है
      mutex लगाकर जबरन fit करने पर आमतौर पर दूसरे problems और bottlenecks पैदा होते हैं
      immutable objects इस्तेमाल करने हैं तो structural sharing का उपयोग करने वाले immutable data structures इस्तेमाल करने चाहिए, ताकि memory usage के explosion से बचा या उसे घटाया जा सके
      pure functional data structures इस्तेमाल करने से समस्या का एक हिस्सा गायब हो जाता है, लेकिन अगर threads एक-दूसरे द्वारा बनाई intermediate versions पर निर्भर करते हैं, तो एक और सिरदर्द पैदा होता है और कोई दूसरा data structure चाहिए हो सकता है
      अगर पहले से इस्तेमाल हो रहे mutable data structure को जबरन बचाना हो, तो access attempts को data structure तक पहुंचने से पहले serialize किया जा सकता है और access को transactions में बांधकर सिर्फ complete transactions चलने दिए जा सकते हैं; ऐसा करते-करते लगभग database implement करने जैसा लगने लगता है
    • shared memory environment में independent execution करने की कोशिश करने वाला threading model ही मुझे मूल रूप से गलत लगता है
      ऐसी चीज को चलाने में लगी मेहनत बेहतर process model में लगनी चाहिए थी, ऐसा मेरा मानना है
  • मूल लेख में code को simplify किया गया example सटीक नहीं है
    original code unrelatedObjects खाली न होने पर ही treeMap.put करता है, और यह फर्क bug भी हो सकता है और नहीं भी
    यह भी verify करना चाहिए कि a और b हर बार वही value लौटाते हैं या नहीं, और treeMap सच में सिर्फ map की तरह ही behave करता है या नहीं
    उदाहरण के लिए, अगर map updates को log करता है, तो इसे केवल एक बार log करने के लिए बदलना ठीक है या नहीं, यह देखना होगा

    • सही point है। खाली न होने की जांच करने वाले if में बदलना चाहिए
  • Comparator या Comparable implementation consistent total order provide न करे, तब भी infinite loop हो सकता है: https://stackoverflow.com/questions/62994606/concurrentskips...
    इसका concurrency से कोई संबंध नहीं है
    यह होगा या नहीं, यह process किए जा रहे specific data और processing order पर depend कर सकता है, इसलिए लंबे समय तक normal दिखने के बाद production environment में फट सकता है

    • उत्सुकता है कि क्या आपने सच में इसे खुद देखा है। अच्छा blog post material बन सकता है
      total order न देने वाला buggy comparator मुझे व्यक्तिगत रूप से अभी तक नहीं मिला
  • मुझे लगता था कि race condition सिर्फ डेटा करप्शन या deadlock पैदा करती है, यह नहीं सोचा था कि यह performance problem भी बना सकती है
    लेकिन अगर यह डेटा को corrupt करके infinite loop बना सकती है, तो बात समझ आती है
    इसलिए मैं अक्सर सोचता हूँ कि project की errors, अजीब behavior और warnings को सिद्धांततः fix करना चाहिए। क्योंकि वे ऊपर से असंबंधित दिखने वाली समस्या पैदा कर सकती हैं
    हालांकि क्या करना है तय करने वालों के बीच यह सिद्धांत स्वीकार होना दुर्लभ है

    • यह ठीक-ठाक rule of thumb है, लेकिन practicality जरूरी है
      कुछ projects में सभी errors, अजीब behavior और warnings हटाने की लागत, कभी-कभार आने वाली ऊपर से असंबंधित दिखने वाली समस्या झेलने की लागत से कहीं ज्यादा हो सकती है
      किसी खास error के भविष्य की समस्या में शामिल होने की संभावना और उसे पहले से fix करना सस्ता पड़ेगा या नहीं—यह लगभग predict करना असंभव है, इसलिए आखिरकार यह science से ज्यादा craft जैसा हो जाता है
      “कुछ भी fix न करना” भयानक है और “सब कुछ fix करना” आम तौर पर अवास्तविक है, इसलिए decision-making framework या अनुभव से आई gut feeling, और stakeholders का भरोसा चाहिए
    • कार्यस्थल पर हम सभी warnings को errors मानते हैं, और जब तक automatically चलने वाली सभी CI pipelines pass नहीं होतीं, pull request merge नहीं किया जा सकता
      discipline चाहिए, लेकिन एक बार उस state में पहुँचा दें तो maintain करना कहीं आसान हो जाता है
    • exceptions को catch करके ignore न करना भी महत्वपूर्ण है
      जब तक आपको specifically यह न पता हो कि program execution जारी रखना safe है, “catch करके सिर्फ log कर देना” भी बुरा idea है
      बेहतर है exception को propagate होने दें, ताकि वह ऐसी जगह तक पहुँचे जहाँ useful handling हो सके—जैसे 500 return करना या error dialog दिखाना
    • आखिरकार आपको अपनी लड़ाइयाँ चुननी पड़ती हैं
      एक Rails project जिसे हम maintain करते थे, उसमें logs “unsupported parameters” से भरे पड़े थे। controllers को बारीकी से check करके allow करने के बाद भी यह आता रहा
      शायद harmless था, लेकिन logs में बहुत noise पैदा करता था
      कई लोगों ने इसे solve करने की कोशिश की, लेकिन हमेशा higher priority काम थे, और किसी feature impact न रखने वाली चीज पर बहुत समय खर्च करना justify करना भी मुश्किल था
      यह बवासीर जैसी परेशान करने वाली समस्या है। surgery कराके कुछ हफ्ते बहुत दर्द झेलना है या बस सहकर जीना है—ऐसा चुनाव; आम तौर पर benign, लेकिन बिगड़कर बड़ी समस्या बनने की संभावना भी होती है
      ऐसे phenomenon को digital hemorrhoids कहना ठीक लगेगा
    • बिना corruption के भी race condition वही काम कई बार करवाकर और सिर्फ एक result छोड़कर बड़ी performance problem पैदा कर सकती है
      अगर आपने warning को unrelated माना है, तो कम से कम comment में explain करें, और संभव हो तो warning को जितने narrow scope में हो उतने में बंद करने वाला pragma लगाएँ
      अजीब behavior को हटाना मुझे ज्यादा पसंद है। “क्यों काम करता है पता नहीं” के रूप में marked code बाद में काम करना बंद कर चुका है, और अगर पहले साफ किया होता तो सावधानी से fix करने का समय होता, लेकिन हमें जल्दबाजी में फिर से लिखना पड़ा
  • “मुश्किल से ssh से connect किया” वाला हिस्सा देखकर मुझे grad school का एक वाकया याद आया
    हमारी lab में parallel/distributed processing पर काम करने वाला लगभग 8 लोगों का छोटा group था, जो शायद Sun UltraSparc 170 मशीन share करता था; hard disk 1GB और RAM 128 या 256MB के आसपास थी
    यह भी ध्यान रखना चाहिए कि Sun machines को लगभग reboot नहीं किया जाता था
    लगता है एक नए student ने बड़ी text file को line numbers के आधार पर 32 या 64 ranges में बाँटा, और अलग files बनाए बिना perl की N copies parallel चलाकर हर copy से अपना line range process करवाया
    उस समय के हिसाब से perl interpreters की N copies ने काफी RAM खा ली, और swap शुरू होते ही वे उसी file के अलग-अलग हिस्सों पर पागलों की तरह seek करते हुए हर process कुछ और lines पढ़ने की कोशिश कर रहा था
    ऊपर से Jवें process को अपने section तक पहुँचने के लिए file का J/N हिस्सा पढ़ना पड़ा होगा
    console पर login prompt भी नहीं मिल रहा था, लेकिन सौभाग्य से एक session पहले से logged in था और su ने 20–30 मिनट बाद password prompt दिखाया
    5–10 मिनट और बाद root session मिला, top से कारण देखा, user से contact करने की कोशिश की और problematic processes kill किए, तब system normal हुआ
    idea खुद सही था, लेकिन system limits की बिल्कुल समझ नहीं थी; hard disk और कम RAM के कारण I/O bottleneck इतना गंभीर था कि simple linear processing कहीं बेहतर रहती

  • Java हो या कोई और language, thread-safe न होने वाले object पर concurrent operations करने से दुनिया के सबसे interesting bugs निकलते हैं

    • इसलिए thread-safe न होने वाले object को कई threads से इस्तेमाल करते समय atomic access खुद manage करना चाहिए, या thread-safe version इस्तेमाल करना चाहिए
      multithreading errors debug करने में सबसे खराब होती हैं
      इस case में इसे design time पर पहचानना बहुत आसान था, और multithreaded environment में normal container इस्तेमाल करने की कोशिश करते ही warning light जल जानी चाहिए थी
    • concurrency bugs काफी झेले हैं, लेकिन Java के mutable field से volatile modifier हटाने पर होने वाली inconsistency को मैंने जानबूझकर reproduce नहीं किया
      शायद उस समय इस्तेमाल किया गया JVM बहुत अच्छी तरह बनाया गया था
    • जब मुझे पूरा भरोसा था कि unsynchronized चीज share करने के नतीजे झेल सकता हूँ, तब भी actual results हमेशा उम्मीद से बाहर निकले
    • थोड़ा extreme विचार है, लेकिन इसी वजह से मैं C/C++ में race debugging को ज्यादा prefer करता हूँ
      language race होने पर practically बेअर्थ पागल semantics define करती है, लेकिन असल में memory corruption या noisy problems अक्सर सामने आ जाती हैं
      क्योंकि races ज्यादातर illegal हैं, semantics दिखाने के लिए source code बदले बिना thread checker जैसे tools बनाए जा सकते हैं
      इसके उलट Java में undefined behavior नहीं है, लेकिन दो fields के subtly mismatch होने जैसी बातें आसानी से हो सकती हैं, और इन्हें track करना कहीं ज्यादा मुश्किल है
    • Java Collections के कुछ, शायद ज्यादातर operations ऐसी problems की warning देने के लिए integrity checks करते हैं
      उदाहरण के लिए map ConcurrentModificationException throw करता है
  • production में चल रहे C# में भी मैंने यही phenomenon देखा है
    symptoms वही थे और process dump देखने पर corrupted dictionary मिली
    मुझे लगा था कि सब जगह ConcurrentDictionary इस्तेमाल हो रहा है, लेकिन library से आया एक instance problem था
    उस समय .NET Framework इस्तेमाल कर रहे थे, और जहाँ तक याद है .NET Core में concurrent modification detect करने वाला code है
    implementation ठीक से नहीं जानता, लेकिन सिर्फ version counter हो तो भी संभव होगा
    NPE को जैसे key ingredient मानकर obsession दिखाना अजीब है। original occurrence में वह दिखता नहीं, और सिर्फ exception न होने के कारण यह मानने की वजह नहीं कि C में ऐसे bugs नहीं होते
    core बात class invariants है। आम तौर पर mutator चलने के दौरान invariant valid नहीं रहता और अंत में ही restore होता है
    invariant restore होने से पहले दूसरा mutator चलाएँ तो data structure corrupt हो जाता है। अगर valid state से शुरू नहीं करते, तो अंत में valid state में होने की संभावना कम है

    • आखिरकार यह गलत logic की वजह से था
      uncaught NPE से reproduce न हो पाने की बदकिस्मती हुई, इसलिए गलत conclusion निकाल लिया कि uncaught NPE necessary condition है
  • पर्याप्त synchronization न होने वाले java.util.HashMap में भी मैंने यही चीज़ देखी है
    यह करीब 2009 की बात थी, लेकिन जहाँ तक मुझे पता है आज भी संभव है
    मेरी याद के मुताबिक HashMap collision resolution के लिए chaining इस्तेमाल करता है, और शायद chain के अंदर कोई cycle बन गई थी
    हालांकि इसे verify करने के लिए गहराई में जाने के बजाय मैंने problem code को पूरी तरह हटाने पर ध्यान दिया
    Concurrency knowledge अक्सर interviews में पूछा जाता है, और अगर आप data race को छोटी-मोटी समस्या समझते हैं तो अच्छा impression बनाना मुश्किल है; यह case उसकी वजह बताता है

    • मुझे नहीं पता था कि HashMap में भी ऐसा हो सकता है
      क्या यह collision handling में इस्तेमाल होने वाली linked list से जुड़ा है?
  • Cycle खोजने के लिए एक incrementing counter रखना, और अगर वह tree depth या collection size से बड़ा हो जाए तो exception throw करना कैसा रहेगा, यह सोच रहा हूँ
    लेखक ने जो hash set वाला तरीका सुझाया है, उसके उलट इसमें memory या CPU overhead लगभग नहीं है और इसके accept होने की संभावना भी ज़्यादा लगती है
    हालांकि C# को 10 साल से ज़्यादा इस्तेमाल करते हुए ऐसा कभी नहीं हुआ कि concurrent situation में data structure के concurrent access पर विचार न किया हो

    • बुरा idea नहीं है, लेकिन data race वाली situation में useful precondition जोड़ना आम तौर पर काफी मुश्किल होता है
      Tree के corrupt होने के तरीके इसके अलावा भी बहुत हैं
    • अच्छा idea है। Binary search और tree structures में पहले ऐसा कर चुका हूँ
      जहाँ संभव हो infinite loop से बचें, और जिन cases में नहीं बच सकते वे काफी rare होते हैं
      यह fix नहीं है, लेकिन अच्छी mitigation strategy है
      Infinite loop सबसे खराब bugs में से एक है। Debugger में इसे ढूँढना आसान है, लेकिन OP की “मुश्किल से ssh चल पा रहा था” वाली situation जैसे हालात में इसका नतीजा बहुत unfriendly हो सकता है
      खासकर library code में infinite loop और भी परेशान करने वाला होता है
    • वही कहीं बेहतर है। सिर्फ constant memory चाहिए
      यह guarantee होती है कि nodes की संख्या tree height से ज़्यादा नहीं है
  • Thread के अंदर exceptions सच में घातक होते हैं
    C++, select(), और exceptions इधर-उधर फेंकने वाले thread की मुख्य भूमिका वाली एक डरावनी bug-tracking story है: https://news.ycombinator.com/item?id=42532979

    • मुझे याद है कि मैंने वह पोस्ट पढ़ी थी, लेकिन उस domain की knowledge कम होने की वजह से समझ नहीं पाया था
      फिर से पढ़नी पड़ेगी