2025, Oct 31 04:02

Python में Firestore ट्रांज़ैक्शन: stream() को ट्रांज़ैक्शन के बाहर रखें

जानें क्यों Python में Firestore ट्रांज़ैक्शन के भीतर stream() जैसी non-transactional reads खतरनाक हैं, retries बढ़ाती हैं और atomicity को कमजोर करती हैं।

Python ट्रांज़ैक्शन के अंदर stream() जैसी Firestore क्वेरी चलाना तब बेख़तर लग सकता है जब सब कुछ “जैसा चाहिए वैसा” चलता है। लेकिन इसमें एक बारीक जाल छुपा है: अगर वे reads ट्रांज़ैक्शन ऑब्जेक्ट के जरिए नहीं जातीं, तो आप मूल transactional गारंटी से बाहर हो जाते हैं और अनजाने में ट्रांज़ैक्शन को धीमा और फिज़ूल बना सकते हैं। यह सलाह अब भी लागू होती है कि ट्रांज़ैक्शन के भीतर non-transactional पढ़ाई/लिखाई से बचें—भले ही आपका कोड क्रैश न करे।

समस्या का उदाहरण

नीचे दिया स्निपेट एक ट्रांज़ैक्शन के अंदर सभी मौजूद markdown chunks हटाता है और नए लिखता है। हटाने की सूची एक सामान्य collection stream() से आती है जो दिए गए ट्रांज़ैक्शन ऑब्जेक्ट का उपयोग नहीं करती।

def write_md_chunks(txn: firestore.Transaction, note_doc: firestore.DocumentReference, md_text: str):
    md_coll = note_doc.collection('markdowns')
    # मौजूदा चंक्स हटाएँ (ट्रांज़ैक्शन के अंदर नॉन-ट्रांज़ैक्शनल रीड)
    existing = md_coll.stream()
    for snap in existing:
        target = md_coll.document(snap.id)
        txn.delete(target)
        print("EXPECTED A CRASH, BUT IT DIDN'T. WHY?")
    pieces = helpers.split_text(md_text, CHUNK_LIMIT)
    for idx, part in enumerate(pieces):
        part_doc = md_coll.document()
        txn.set(part_doc, {
            'text': part,
            'order': idx + 1
        })
@http_fx.on_request(
    cors=cors_opts.CorsOptions(
        cors_origins=["*"],
        cors_methods=["POST"],
    )
)
def handle_md_edit(req: http_fx.Request) -> http_fx.Response:
    if req.method != 'POST':
        return http_fx.Response(
            json.dumps({"error": "Only POST requests are allowed"}),
            status=405
        )
    payload = req.get_json(silent=True)
    if not payload:
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    user_id = payload['data']['uid']
    note_id = payload['data']['doc_id']
    next_md = payload['data']['markdown']
    if helpers.is_blank_or_none(next_md):
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    if len(next_md) > 524288:
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    db = firestore.client()
    now_ms = int(time.time() * 1000)
    try:
        @firestore.transactional
        def apply_note_update(txn):
            note_doc = (
                db.collection('users')
                .document(user_id)
                .collection('notes')
                .document(note_id)
            )
            current_md = md_tools.get_markdown(
                transaction=txn,
                note_ref=note_doc
            )
            if next_md != current_md:
                original_md = md_tools.get_original_markdown(
                    transaction=txn,
                    note_ref=note_doc
                )
                if next_md == original_md:
                    md_tools.delete_original_markdown(
                        transaction=txn,
                        note_ref=note_doc
                    )
                else:
                    md_tools.insert_original_markdown_if_not_exist(
                        transaction=txn,
                        note_ref=note_doc,
                        original_markdown=current_md
                    )
                md_tools.update_markdown(
                    transaction=txn,
                    note_ref=note_doc,
                    markdown=next_md
                )
                txn.update(
                    note_doc,
                    {
                        'modified_timestamp': now_ms,
                        'synced_timestamp': now_ms
                    }
                )
        txn = db.transaction()
        apply_note_update(txn)
        response = {
            "data": {
                "modified_timestamp": now_ms,
                "synced_timestamp": now_ms
            }
        }
        return http_fx.Response(
            json.dumps(response),
            status=200
        )
    except Exception as err:
        print(f"Error updating note markdown: {str(err)}")
        return http_fx.Response(
            json.dumps({"data": {"error": f"An error occurred: {str(err)}"}}),
            status=500
        )

यहाँ असल समस्या क्या है

मूल दिक्कत यह नहीं है कि stream() अपने आप में अवैध है। समस्या है कोई भी डेटाबेस read या write जो ट्रांज़ैक्शन के दायरे में तो होता है, पर दिए गए ट्रांज़ैक्शन ऑब्जेक्ट से होकर नहीं जाता। इसे टालने के दो कारण हैं। पहला, ट्रांज़ैक्शन को तेज़ चलना चाहिए ताकि कंटेंशन कम हो। ऐसी क्वेरी चलाना जिनका ट्रांज़ैक्शन समन्वय नहीं करता, उसे धीमा कर सकता है। दूसरा, कंटेंशन होने पर ट्रांज़ैक्शन कई बार retry कर सकता है। अगर आप ट्रांज़ैक्शन बॉडी के भीतर non-transactional कॉल से read करते हैं, तो वे reads बार‑बार और बेकार में हो सकती हैं। साथ ही, उन reads पर ऑटो‑retry का लाभ भी खो देते हैं, जिससे atomicity कमज़ोर पड़ती है।

यही वजह है कि आपको क्रैश नहीं दिखा। SDK के लिए रनटाइम पर यह पकड़ पाना कि आपने सक्रिय ट्रांज़ैक्शन के साथ non-transactional reads मिला दी हैं—खासतौर पर मल्टी‑थ्रेडिंग में—न भरोसेमंद है, न कम‑ओवरहेड। इसलिए बेस्ट प्रैक्टिस मानना आपकी ज़िम्मेदारी है: जो काम ट्रांज़ैक्शनल है, उसे ट्रांज़ैक्शनल ही रखें।

इसे कैसे सुधारें

दो सुरक्षित तरीके हैं। या तो डेटा को ट्रांज़ैक्शन ऑब्जेक्ट से पढ़ें ताकि उन डॉक्यूमेंट्स में बदलाव सही तरह से retry ट्रिगर करें और atomicity बनी रहे, या फिर वे reads पूरी तरह ट्रांज़ैक्शन के बाहर करें ताकि वे ट्रांज़ैक्शनल रन को धीमा और जटिल न बनाएं।

नीचे संशोधित उदाहरण stream() कॉल को ट्रांज़ैक्शन से बाहर ले आता है। ट्रांज़ैक्शन फिर IDs से delete करता है और नए chunks लिखता है। कुल व्यवहार वही रहता है, लेकिन अब non-transactional read ट्रांज़ैक्शन बॉडी के भीतर नहीं होती।

def put_md_chunks_precomputed(txn: firestore.Transaction,
                              note_doc: firestore.DocumentReference,
                              md_text: str,
                              chunk_ids: list[str]):
    md_coll = note_doc.collection('markdowns')
    # ज्ञात चंक डॉक्यूमेंट्स को ट्रांज़ैक्शन के अंदर हटाएँ
    for cid in chunk_ids:
        txn.delete(md_coll.document(cid))
    parts = helpers.split_text(md_text, CHUNK_LIMIT)
    for idx, part in enumerate(parts):
        new_doc = md_coll.document()
        txn.set(new_doc, {
            'text': part,
            'order': idx + 1
        })
@http_fx.on_request(
    cors=cors_opts.CorsOptions(
        cors_origins=["*"],
        cors_methods=["POST"],
    )
)
def handle_md_edit(req: http_fx.Request) -> http_fx.Response:
    if req.method != 'POST':
        return http_fx.Response(
            json.dumps({"error": "Only POST requests are allowed"}),
            status=405
        )
    payload = req.get_json(silent=True)
    if not payload:
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    user_id = payload['data']['uid']
    note_id = payload['data']['doc_id']
    next_md = payload['data']['markdown']
    if helpers.is_blank_or_none(next_md):
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    if len(next_md) > 524288:
        return http_fx.Response(
            json.dumps({"error": "Invalid request data"}),
            status=400
        )
    db = firestore.client()
    now_ms = int(time.time() * 1000)
    # ट्रांज़ैक्शन के बाहर पहले से पढ़ लें
    note_doc = (
        db.collection('users')
        .document(user_id)
        .collection('notes')
        .document(note_id)
    )
    md_coll = note_doc.collection('markdowns')
    to_delete_ids = [snap.id for snap in md_coll.stream()]
    try:
        @firestore.transactional
        def apply_note_update(txn):
            # स्पष्टता के लिए अंदर वही note_doc दोबारा तैयार करें
            nd = (
                db.collection('users')
                .document(user_id)
                .collection('notes')
                .document(note_id)
            )
            current_md = md_tools.get_markdown(transaction=txn, note_ref=nd)
            if next_md != current_md:
                original_md = md_tools.get_original_markdown(transaction=txn, note_ref=nd)
                if next_md == original_md:
                    md_tools.delete_original_markdown(transaction=txn, note_ref=nd)
                else:
                    md_tools.insert_original_markdown_if_not_exist(
                        transaction=txn,
                        note_ref=nd,
                        original_markdown=current_md
                    )
                # पहले से पढ़े गए IDs के आधार पर चंक्स बदलें
                put_md_chunks_precomputed(txn, nd, next_md, to_delete_ids)
                txn.update(nd, {
                    'modified_timestamp': now_ms,
                    'synced_timestamp': now_ms
                })
        txn = db.transaction()
        apply_note_update(txn)
        return http_fx.Response(
            json.dumps({
                "data": {
                    "modified_timestamp": now_ms,
                    "synced_timestamp": now_ms
                }
            }),
            status=200
        )
    except Exception as err:
        print(f"Error updating note markdown: {str(err)}")
        return http_fx.Response(
            json.dumps({"data": {"error": f"An error occurred: {str(err)}"}}),
            status=500
        )

यह बात याद रखने लायक क्यों है

ट्रांज़ैक्शन दोबारा चल सकते हैं, और उन्हें हल्का रखना चाहिए। ट्रांज़ैक्शन बॉडी के अंदर रहते हुए ट्रांज़ैक्शन ऑब्जेक्ट के बिना Firestore को पढ़ना या लिखना इन लक्ष्यों के ख़िलाफ़ जाता है। आपको ज़रूरी नहीं कि रनटाइम एरर मिले, और इसे अतिरिक्त ओवरहेड जोड़े बिना SDK के लिए स्वतः लागू कर पाना भी व्यावहारिक नहीं है। यह अनुशासन का मामला है—अपना ट्रांज़ैक्शनल कोडपाथ साफ़ और अनुमानित रखें ताकि retries और atomicity आपके पक्ष में काम करें।

मुख्य बातें

अगर आपको ऐसा डेटा पढ़ना है जो ट्रांज़ैक्शन के नतीजे को प्रभावित करता है, तो उसे ट्रांज़ैक्शन ऑब्जेक्ट से पढ़ें। अगर सिर्फ IDs की सूची या ऐसा संदर्भ चाहिए जिसे ट्रांज़ैक्शनल समन्वय नहीं चाहिए, तो उसे ट्रांज़ैक्शन शुरू होने से पहले हासिल करें। क्रैश न होने को सुरक्षित पैटर्न का संकेत न मानें। सलाह स्पष्ट है: ट्रांज़ैक्शन के भीतर non-transactional Firestore reads और writes से बचें।

यह लेख StackOverflow पर प्रश्न (लेखक: Cheok Yan Cheng) और Doug Stevenson के उत्तर पर आधारित है।