ধীর সংযোগের জন্য Kotlin নেটওয়ার্কিং: টাইমআউট ও নিরাপদ রিট্রাই
ধীর সংযোগের জন্য ব্যবহারিক Kotlin নেটওয়ার্কিং: টাইমআউট নির্ধারণ করুন, নিরাপদভাবে ক্যাশ করুন, নকল ছাড়া রিট্রাই করুন এবং অনিশ্চিত মোবাইল নেটওয়ার্কে গুরুত্বপূর্ণ কাজগুলো রক্ষা করুন।

ধীর ও অনির্দেশ্য সংযোগে কী ভেঙে পড়ে
মোবাইলে “ধীর” মানে সাধারণত “ইন্টারনেট নেই” নয়। এটা প্রায়ই এমন একটি সংযোগ বোঝায় যা কাজ করে, কিন্তু কেবল স্বল্প সময়ের জন্য। একটি অনুরোধ 8–20 সেকেন্ড লাগতে পারে, মাঝপথে স্থগিত হতে পারে, তারপর শেষ হবে। অথবা এক মুহূর্তে সফল হয় এবং পরের মুহূর্তে ব্যর্থ, কারণ ফোনটি Wi‑Fi থেকে LTE-তে স্যুইচ করেছে, নিম্ন-সিগন্যাল এলাকায় প্রবেশ করেছে, অথবা OS অ্যাপকে ব্যাকগ্রাউন্ডে ঠেলেছে।
“ফ্লোকি” আরও খারাপ। প্যাকেট হারায়, DNS টাইমআউট হয়, TLS হ্যান্ডশেক ব্যর্থ হয়, এবং কনেকশন অনিয়মিতভাবে রিসেট হয়। আপনি কোডে সব কিছু “সঠিকভাবে” করলেও মাঠে ফেলতে সমস্যা দেখা দিতে পারে কারণ নেটওয়ার্ক আপনার নীচে বদলাচ্ছে।
এখানেই ডিফল্ট সেটিংস ভাঙতে শুরু করে। অনেক অ্যাপ টাইমআউট, রিট্রাই এবং ক্যাশিংয়ের জন্য লাইব্রেরির ডিফল্টসের উপর নির্ভর করে, বাস্তবে লোকেদের জন্য কি “ভালো পর্যাপ্ত” তা ঠিক করে না। ডিফল্ট প্রায়ই স্থিতিশীল Wi‑Fi এবং দ্রুত API-এর জন্য টিউন করা থাকে, না কমিউটার ট্রেন, লিফট বা ব্যস্ত কফি শপের জন্য।
ব্যবহারকারীরা “সকেট টাইমআউট” বা “HTTP 503” বলে বর্ণনা করে না। তারা লক্ষণগুলো লক্ষ্য করে: অনন্ত স্পিনার, দীর্ঘ অপেক্ষার পরে হঠাৎ ত্রুটি (তারপর পরবর্তী চেষ্টায় কাজ করে), ডুপ্লিকেট ক্রিয়াকলাপ (দুইটি বুকিং, দুইটি অর্ডার, দ্বিগুণ চার্জ), হারানো আপডেট, এবং মিশ্র স্টেট যেখানে UI বলে “অসফল” কিন্তু সার্ভার আসলে সফল হয়েছে।
ধীর নেটওয়ার্ক সামান্য ডিজাইনের ফাঁককে অর্থ ও বিশ্বাসের সমস্যায় পরিণত করে। যদি অ্যাপ স্পষ্টভাবে “এখনো পাঠানো হচ্ছে”, “ব্যর্থ” এবং “সম্পন্ন” আলাদা না করে, ব্যবহারকারী আবার ট্যাপ করে। ক্লায়েন্ট যদি অচিন্তাভাবেই রিট্রাই করে, তাহলে ডুপ্লিকেট তৈরি হতে পারে। সার্ভার যদি idempotency সমর্থন না করে, তখন এক দুর্বল সংযোগ বহু “সফল” লেখন তৈরি করতে পারে।
“গুরুত্বপূর্ণ অ্যাকশন” বলতে এমন কিছু বোঝায় যা সর্বাধিক একবার ঘটতে হবে এবং সঠিক হতে হবে: পেমেন্ট, চেকআউট সাবমিশন, স্লট বুকিং, পয়েন্ট ট্রান্সফার, পাসওয়ার্ড পরিবর্তন, শিপিং ঠিকানা সংরক্ষণ, একটি ক্লেইম জমা বা অনুমোদন পাঠানো।
বাস্তব উদাহরণ: কেউ দুর্বল LTE তে চেকআউট সাবমিট করে। অ্যাপ অনুরোধ পাঠায়, তারপর রেসপন্স আসার আগে সংযোগ কেটে যায়। ব্যবহারকারী একটি ত্রুটি দেখে, আবার “Pay” ট্যাপ করে, এবং এখন দুটি অনুরোধ সার্ভারে পৌঁছে। স্পষ্ট নিয়ম না থাকলে, অ্যাপ বলতে পারে না retry করা উচিত, অপেক্ষা করা উচিত, নাকি বন্ধ করা উচিত। ব্যবহারকারী বলতে পারে না তারা আবার চেষ্টা করা উচিত কি না।
কোড টিউন করার আগে নিয়ম ঠিক করুন
কনেকশন ধীর বা অনির্দেশ্য হলে, বেশিরভাগ বাগ অনির্দিষ্ট নিয়ম থেকে আসে, HTTP ক্লায়েন্ট থেকে নয়। টাইমআউট, ক্যাশিং বা রিট্রাই স্পর্শ করার আগে লিখে রাখুন আপনার অ্যাপের জন্য “সঠিক” কী।
যেগুলো কখনই দুইবার চলা চলবে না সেগুলো থেকে শুরু করুন। সাধারণত এগুলো অর্থ ও অ্যাকাউন্ট সম্পর্কিত: অর্ডার প্লেস, কার্ড চার্জ, পে‑আউট সাবমিট, পাসওয়ার্ড পরিবর্তন, অ্যাকাউন্ট মুছা। যদি ব্যবহারকারী দুইবার ট্যাপ করে বা অ্যাপ রিট্রাই করে, সার্ভারকে তা একটাই অনুরোধ মনে করতে হবে। যদি আপনি এখনও সেটা নিশ্চিত করতে না পারেন, ওই endpoints‑গুলোকে “অটোম্যাটিক রিট্রাই নয়” হিসেবে বিবেচনা করুন যতক্ষণ না আপনি নিরাপত্তা নিশ্চিত করেন।
পরের ধাপে সিদ্ধান্ত নিন প্রতিটি স্ক্রিন নেটওয়ার্ক খারাপ হলে কী করতে পারবে। কিছু স্ক্রিন অফলাইনেও কাজে লাগতে পারে (শেষ দেখা প্রোফাইল, পূর্বের অর্ডার)। অন্যগুলো রিড-ওনলি বা স্পষ্ট “আবার চেষ্টা করুন” স্টেট দেখানো উচিত (ইনভেন্টরি কাউন্ট, লাইভ প্রাইসিং)। এই আশা মিক্স করলে বিভ্রান্ত UI এবং ঝুঁকিপূর্ণ ক্যাশিং হবে।
প্রতিটি অ্যাকশনের জন্য গ্রহণযোগ্য অপেক্ষার সময়টি ব্যবহারকারীরা কিভাবে ভাবেন তার উপর নির্ধারণ করুন, কোডে কী সুন্দর লাগে তার উপর নয়। লগইন স্বল্প অপেক্ষা সহ্য করতে পারে। ফাইল আপলোডকে বেশি সময় দিন। চেকআউট দ্রুত হলেও নিরাপদ অনুভূত হওয়া উচিত। 30 সেকেন্ডের টাইমআউট কাগজে “রিলায়েবল” হতে পারে কিন্তু ব্যবহারকারীর দৃষ্টিতে ভাঙ্গা মনে হতে পারে।
অবশেষে সিদ্ধান্ত নিন আপনি ডিভাইসে কী সংরক্ষণ করবেন এবং কতক্ষণ। ক্যাশ করা ডেটা সাহায্য করে, কিন্তু পুরনো ডেটা ভুল সিদ্ধান্তে নিয়ে যেতে পারে (পুরনো দাম, মেয়াদোত্তীর্ণ অযোগ্যতা)।
একটা README-র মতো জায়গায় নিয়মগুলি লিখে রাখুন যেখানে সবাই খুঁজে পাবে। সহজ রাখুন:
- কোন endpoint‑গুলো “ডুপ্লিকেট করা যাবেনা” এবং idempotency হ্যান্ডলিং দরকার?
- কোন স্ক্রিনগুলো অফলাইন কাজ করবে, আর কোনগুলো অফলাইন হলে রিড‑ওনলি হবে?
- প্রতিটি অ্যাকশনের সর্বোচ্চ অপেক্ষার সময় কি (লগইন, ফিড রিফ্রেশ, আপলোড, চেকআউট)?
- কি ডেটা ডিভাইসে ক্যাশ করা যাবে, এবং এর এক্সপায়ারি কত?
- ফেইল হলে আপনি ত্রুটি দেখাবেন, পরে কিউ করবেন, নাকি ইউজারকে ম্যানুয়ালি আবার চেষ্টা করতে বলবেন?
নিয়মগুলো স্পষ্ট হলে আপনার টাইমআউট মান, ক্যাশিং হেডার, রিট্রাই নীতি, এবং UI স্টেট বাস্তবায়ন ও টেস্ট করা অনেক সহজ হবে।
ব্যবহারকারীর প্রত্যাশার সঙ্গে মিল রেখে টাইমআউট
ধীর নেটওয়ার্ক বিভিন্নভাবে ব্যর্থ হয়। একটি ভাল টাইমআউট সেটআপ কেবল “একটি সংখ্যা” না নিয়ে, ব্যবহারকারী কী করতে চাইছে তা প্রতিফলিত করে এবং পর্যাপ্ত দ্রুত ব্যর্থ হয় যাতে অ্যাপ পুনরুদ্ধার করতে পারে।
তিনটি টাইমআউট, সহজ ভাষায়:
- Connect timeout: সার্ভারের সাথে সংযোগ স্থাপনের জন্য কতক্ষণ অপেক্ষা করবেন (DNS লুকআপ, TCP, TLS)। যদি এটা ব্যর্থ হয়, অনুরোধ বাস্তবে শুরু হয়নি।
- Write timeout: রিকুয়েস্ট বডি পাঠানোর সময় (আপলোড, বড় JSON, ধীর আপলিঙ্ক)।
- Read timeout: রিকুয়েস্ট পাঠানোর পরে সার্ভার থেকে ডেটা পাওয়ার জন্য কতক্ষণ অপেক্ষা করবেন। এটি স্পট্টি মোবাইল নেটওয়ার্কে প্রায়ই দেখা যায়।
টাইমআউটগুলো স্ক্রিন ও স্টেকস অনুযায়ী নির্ধারণ করুন। একটি ফিড ধীরে হলে তেমন ক্ষতি নেই। একটি গুরুত্বপূর্ণ অ্যাকশন বা তো সম্পন্ন হবে বা স্পষ্টভাবে ব্যর্থ হবে যাতে ব্যবহারকারী সিদ্ধান্ত নিতে পারে।
প্রায়োগিক শুরু পয়েন্ট (পরিমাপ করে সমন্বয় করুন):
- List loading (নিম্ন ঝুঁকি): connect 5–10s, read 20–30s, write 10–15s.
- Search-as-you-type: connect 3–5s, read 5–10s, write 5–10s.
- Critical actions (উচ্চ ঝুঁকি, যেমন “Pay” বা “Submit order”): connect 5–10s, read 30–60s, write 15–30s.
সামঞ্জস্যতা নিখুঁততার চেয়ে বেশি গুরুত্বপূর্ণ। যদি ব্যবহারকারী “Submit” ট্যাপ করে এবং দুই মিনিট স্পিনার দেখে, তারা আবার ট্যাপ করবে।
“অসীম লোডিং” এড়াতে UI-তে একটি স্পষ্ট উপরের সীমা যোগ করুন। তাত্ক্ষণিকভাবে প্রগ্রেস দেখান, ক্যানসেল করার অপশন দিন, এবং (ধরা যাক) 20–30 সেকেন্ড পর “এখনও চেষ্টা করছিই…” দেখান যাতে পুনরায় চেষ্টা বা কানেকশন যাচাই করার অপশন দেয়া হয়। এতে অভিজ্ঞতা সততা বজায় থাকে এমনকি যদি নেটওয়ার্ক লাইব্রেরি এখনও অপেক্ষা করে।
একটি টাইমআউট হলে ডিবাগ প্যাটার্ন পরবর্তী বিশ্লেষণের জন্য পর্যাপ্ত লগ রাখুন, কিন্তু সিক্রেটস লগ করবেন না। উপকারী ক্ষেত্রগুলো হল URL path (পূর্ণ কুয়েরি নয়), HTTP method, স্ট্যাটাস (যদি থাকে), টাইমিং ভাঙ্গন (connect vs write vs read), নেটওয়ার্ক টাইপ (Wi‑Fi, সেলুলার), আনুমানিক সাইজ, এবং একটি request ID যাতে ক্লায়েন্ট লগ সার্ভার লগের সাথে মিলানো যায়।
একটি সহজ, সঙ্গতিপূর্ণ Kotlin নেটওয়ার্ক কনফিগারেশন
ধীর কনেকশনে, ক্লায়েন্ট সেটআপে ছোট অসামঞ্জস্য বড় সমস্যা তৈরি করে। একটি পরিষ্কার বেসলাইন দ্রুত ডিবাগে সাহায্য করে এবং প্রতিটি অনুরোধকে একই নিয়ম দেয়।
এক ক্লায়েন্ট, এক নীতি
প্রায়ই একটি OkHttpClient (Retrofit ব্যবহার করলে) যেখানে সব কনফিগারেশন এক জায়গায় বানানো হয়। ডিফল্ট হেডার (app version, locale, auth token) এবং User-Agent, একবারে টাইমআউট সেট করা (বিভিন্ন কলগুলোতে ছড়িয়ে না রেখে), ডিবাগিংয়ের জন্য লোগিং এবং একটি রিট্রাই সিদ্ধান্ত (যদি থাকে “কোনো অটোম্যাটিক রিট্রাই নেই”) এক জায়গায় রাখুন।
নিচে একটি ছোট উদাহরণ আছে যা কনফিগারেশন এক ফাইলে রাখে (কোড ব্লকটি অপরিবর্তিত রাখুন):
val okHttp = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.callTimeout(30, TimeUnit.SECONDS)
.addInterceptor { chain ->
val request = chain.request().newBuilder()
.header("User-Agent", "MyApp/${BuildConfig.VERSION_NAME}")
.header("Accept", "application/json")
.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttp)
.addConverterFactory(MoshiConverterFactory.create())
.build()
ইউজার-ফেসের সাথে মিল রেখে সেন্ট্রাল ত্রুটি হ্যান্ডলিং
নেটওয়ার্ক ত্রুটি শুধু একটি একচেটিয়া “একটি exception” নয়। যদি প্রতিটি স্ক্রিন আলাদা করে তা হ্যান্ডেল করে, ব্যবহারকারীরা এলোমেলো মেসেজ পাবে।
একটা মাপার (mapper) তৈরি করুন যা ব্যর্থতাগুলোকে কিছু ছোট, ব্যবহারকারী-বান্ধব আউটকামে রূপান্তর করে: কনেকশন নেই/এয়ারপ্লেন মোড, টাইমআউট, সার্ভার ত্রুটি (5xx), ভ্যালিডেশন বা auth ত্রুটি (4xx), এবং একটি অজানা ফ্যালব্যাক।
এতে UI কপিগুলি সঙ্গতিপূর্ণ থাকে ("কানেকশন নেই" বনাম "আবার চেষ্টা করুন") এবং প্রযুক্তিগত বিবরণ ফাঁস হয় না।
স্ক্রীন বন্ধ হলে অনুরোধ ট্যাগ করে তা ক্যানসেল করুন
ফ্লোকি নেটওয়ার্কে, কলসমুহ দেরিতে শেষ হয়ে এমন একটি স্ক্রীন আপডেট করতে পারে যা আর নেই। ক্যানসেলেশনকে একটি স্ট্যান্ডার্ড নিয়ম বানান: যখন একটি স্ক্রীন বন্ধ হয়, তার কাজ থামবে।
Retrofit এবং Kotlin coroutine-এ ViewModel‑এ coroutine scope ক্যানসেল করলে অবিলম্বে অনুবর্তী HTTP কলও ক্যানসেল হয়। নন‑কোর আভাবে, Call‑এর একটি রেফারেন্স রেখে cancel() কল করুন। এছাড়া অনুরোধ ট্যাগ করে কোনো ফিচার থেকে বাহির হলে সংশ্লিষ্ট কল গুলো ক্যানসেল করতে পারেন।
ব্যাকগ্রাউন্ড কাজ UI‑র উপর নির্ভর করা উচিত নয়
যে কোনো গুরুত্বপূর্ণ কাজ যা সম্পন্ন হওয়া অপরিহার্য (রিপোর্ট পাঠানো, কিউ সিঙ্ক করা, সাবমিশন শেষ করা) সেটি এমন একটি শিডিউলার-এ চালান যা এটি সুরক্ষিতভাবে পুনরায় চেষ্টা করতে পারে এবং অ্যাপ রিস্টার্টেও টিকে থাকতে পারে। Android-এ WorkManager সাধারণভাবে ব্যবহৃত হয়। UI অ্যাকশনগুলো হালকা রাখুন এবং যেখানে উপযোগী সেখানে দীর্ঘ কাজ ব্যাকগ্রাউন্ড জব-এ হ্যান্ডঅফ করুন।
মোবাইলে নিরাপদ ক্যাশিং নিয়ম
ক্যাশিং ধীর সংযোগে বড় সুবিধা দেয় কারণ এটি পুনরাবৃত্ত ডাউনলোড কমায় এবং স্ক্রিনকে তাত্ক্ষণিক করে তোলে। কিন্তু ভুল সময়ে পুরনো ডেটা দেখা ভুল সিদ্ধান্তে নিয়ে যেতে পারে—যেমন পুরনো ব্যালান্স বা অপ্রচলিত ডেলিভারি ঠিকানা।
নিরাপদ পদ্ধতি হল কেবল সেই ডেটা ক্যাশ করা যা ব্যবহারকারী একটু পুরনো হলেও সহ্য করবে, এবং অর্থ/নিরাপত্তা/চূড়ান্ত সিদ্ধান্ত প্রভাবিত করে এমন সবকিছুর জন্য তাজা চেক বাধ্যতামূলক করা।
নির্ভরযোগ্য Cache-Control মৌলিক নিয়ম
অধিকাংশ নিয়ম কয়েকটি হেডারের উপর দাঁড়ায়:
max-age=60: 60 সেকেন্ড পর্যন্ত ক্যাশকৃত রেসপন্স ব্যবহার করা যাবে সার্ভারের সাথে পুনরায় যাচাই না করেই।no-store: এই রেসপন্সটি কোনো অবস্থাতেই সংরক্ষণ করবেন না (টোকেন ও সংবেদনশীল স্ক্রিনের জন্য উচিত)।must-revalidate: যদি মেয়াদ শেষ হয়ে যায়, ব্যবহার করার আগে সার্ভারের সাথে যাচাই করা বাধ্যতামূলক।
মোবাইলে, must-revalidate অস্থায়ী অফলাইনের পরে “চুপচাপ ভুল” ডেটা দেখানোরোধ করে। কেউ সাবওয়ে থেকে অ্যাপ খুললে দ্রুত স্ক্রীন চাই, কিন্তু একই সাথে অ্যাপ নিশ্চিত করবে যা এখনও বৈধ।
ETag রিফ্রেশ: দ্রুত, সস্তা এবং নির্ভরযোগ্য
রিড endpoint‑গুলোর জন্য ETag ভিত্তিক ভ্যালিডেশন দীর্ঘ max-age থেকে ভালো হতে পারে। সার্ভার রেসপন্সের সঙ্গে ETag পাঠায়। পরেরবার অ্যাপ If-None-Match সহ ওই মান পাঠায়। কিছু না বদলে থাকলে সার্ভার 304 Not Modified ফেরত দেয়, যা দুর্বল নেটওয়ার্কে ছোট এবং দ্রুত।
এটি প্রোডাক্ট লিস্ট, প্রোফাইল ডিটেইল এবং সেটিংস স্ক্রিনের জন্য ভাল কাজ করে।
সূত্র হিসেবে সহজ নিয়ম:
- পড়া endpoint‑গুলোকে ছোট
max-ageওmust-revalidateদিয়ে ক্যাশ করুন, এবং সম্ভব হলেETagসমর্থন রাখুন। - লেখা endpoint (POST/PUT/PATCH/DELETE) ক্যাশ করবেন না—তাদের সর্বদা নেটওয়ার্ক-বাউন্ড হিসেবে ভাবুন।
- সংবেদনশীল কোনো কিছুর জন্য
no-storeব্যবহার করুন (auth রেসপন্স, পেমেন্ট ধাপ, প্রাইভেট মেসেজ)। - স্ট্যাটিক অ্যাসেট (আইকন, পাবলিক কনফিগ) দীর্ঘ সময় ক্যাশ করুন—স্ট্যালনেসের ঝুঁকি কম।
ক্যাশিং সিদ্ধান্ত অ্যাপ জুড়ে সঙ্গত রাখুন—ব্যবহারকারীরা মিল না থাকলে বেশি লক্ষ্য করে, মিনিটের বিলম্ব নয়।
ক্ষতি বাড়ানো ছাড়া নিরাপদ রিট্রাই
রিট্রাই সহজ সমাধি মনে হলেও এটি ব্যর্থ হলে বিপজ্জনক হতে পারে। ভুল অনুরোধ রিট্রাই করলে অতিরিক্ত লোড তৈরি হয়, ব্যাটারি খায়, এবং অ্যাপ আটকে থাকা অনুভব করতে পারে।
শুরু করুন কেবল সেই ব্যর্থতাগুলো রিট্রাই করে যা অস্থায়ী হওয়ার সম্ভাবনা বেশি। কনেকশন ড্রপ, রিড টাইমআউট বা ছোট সার্ভার আউটেজ পরবর্তী চেষ্টায় সফল হতে পারে। ভুল পাসওয়ার্ড, অনুপস্থিত ফিল্ড বা 404–এ রিট্রাই করবেন না।
প্রায়োগিক নিয়ম:
- টাইমআউট এবং কনেকশন ব্যর্থতা রিট্রাই করুন।
- 502, 503, মাঝে মাঝে 504 রিট্রাই করা যায়।
- 4xx রিট্রাই করবেন না (কিন্তু 408 বা 429‑এ স্পষ্ট অপেক্ষা নিয়ম থাকলে বিবেচনা করা যেতে পারে)।
- এমন অনুরোধ রিট্রাই করবেন না যা ইতিমধ্যে সার্ভারে পৌঁছে প্রক্রিয়াধীন হওয়ার সম্ভাবনা আছে।
- রিট্রাই কম রাখুন (সাধারণত 1–3 চেষ্টাই যথেষ্ট)।
ব্যাকঅফ + জিটার: রিট্রাই স্টর্ম কমায়
যদি অনেক ব্যবহারকারী একই আউটেজে আঘাত করে, একদম দ্রুত রিট্রাই পুনরায় সমান্তরাল ট্র্যাফিক তৈরি করে যা পুনরুদ্ধানকে ধীর করে। এক্সপোনেনশিয়াল ব্যাকঅফ ব্যবহার করুন (প্রতিবার অপেক্ষা বাড়ান) এবং জিটার (স্বল্প রেন্ডম বিলম্ব) যোগ করুন যাতে ডিভাইসগুলো একসাথে রিট্রাই না করে।
উদাহরণ: প্রাথমিক প্রায় 0.5 সেকেন্ড, তারপর 1 সেকেন্ড, তারপর 2 সেকেন্ড—প্রতিবার ±20% র্যান্ডমাইজ করুন।
মোট রিট্রাই সময়ে একটি ক্যাপ রাখুন
সীমা না থাকলে রিট্রাই ব্যবহারকারীকে মিনিটের পর মিনিট স্পিনারে আটকে রাখতে পারে। সম্পূর্ণ অপারেশনের জন্য একটি সর্বোচ্চ মোট সময় নির্ধারণ করুন, রিট্রাই সহ সব অপেক্ষা মিলিয়ে। অনেক অ্যাপ 10–20 সেকেন্ড লক্ষ্য করে, তারপর স্পষ্টভাবে “আবার চেষ্টা করুন” দেখায়।
ওপরের বিষয়কে প্রসঙ্গে মিলান: কেউ ফর্ম সাবমিট করলে তারা দ্রুত উত্তর চায়; ব্যাকগ্রাউন্ড সিঙ্ক ব্যর্থ হলে পরে রিট্রাই করা যেতে পারে।
কখনই অ-আইডেমপোটেন্ট অ্যাকশনে স্বয়ংক্রিয় রিট্রাই করবেন না (অর্ডার, পেমেন্ট) যদি না আপনার কাছে idempotency কী বা সার্ভার-সাইড ডুপ্লিকেট চেকের মতো সুরক্ষা থাকে। যদি নিরাপত্তা নিশ্চয়তা না থাকে, স্পষ্টভাবে ব্যর্থ করুন এবং ব্যবহারকারীকে সিদ্ধান্ত নিতে দিন।
গুরুত্বপূর্ণ অ্যাকশনের ডুপ্লিকেট প্রতিরোধ
ধীর বা অনির্দেশ্য সংযোগে ব্যবহারকারীরা দুইবার ট্যাপ করে। OS ব্যাকগ্রাউন্ডে পুনরায় চেষ্টা করতে পারে। আপনার অ্যাপ টাইমআউট পর থেকে আবার অনুরোধ পাঠাতে পারে। যদি অ্যাকশনটি “কিছু তৈরি করা” হয় (অর্ডার প্লেস, টাকা পাঠানো, পাসওয়ার্ড পরিবর্তন), ডুপ্লিকেট হানা দায়ী হতে পারে।
Idempotency মানে একই অনুরোধের ফলাফল একই হওয়া উচিত। অনুরোধ পুনরাবৃত্ত হলে সার্ভার দ্বিতীয় অর্ডার তৈরি করে না; বরং প্রথম রেজাল্ট ফেরত দেয় বা বলে “আগেই হয়েছে।”
প্রতিটি গুরুত্বপূর্ণ চেষ্টা‑তে idempotency কী ব্যবহার করুন
গুরুত্বপূর্ণ অ্যাকশনের জন্য, ব্যবহারকারী চেষ্টা শুরু করলে একটি ইউনিক idempotency কী জেনারেট করুন এবং অনুরোধের সাথে পাঠান (সাধারণত Idempotency-Key হেডার বা বডির একটি ফিল্ড)।
প্রায়োগিক ফ্লো:
- ব্যবহারকারী “Pay” ট্যাপ করলে একটি UUID idempotency কী তৈরি করুন।
- এটি লোকালি একটি ছোট রেকর্ডে সংরক্ষণ করুন: status = pending, createdAt, request payload hash।
- কীসহ অনুরোধ পাঠান।
- সফল রেসপন্স এলে status = done চিহ্নিত করুন এবং সার্ভারের রেজাল্ট ID সংরক্ষণ করুন।
- রিট্রাই করতে হলে একই কী পুনরায় ব্যবহার করুন, নতুন কী নয়।
এই “একই কী পুনরায় ব্যবহার” নিয়মই দুর্ঘটনাক্রমে দ্বিগুণ চার্জ রোধ করে।
অ্যাপ রিস্টার্ট এবং অফলাইন বিরতি হ্যান্ডল করুন
যদি অ্যাপ মধ্যে অনুরোধ চলাকালীন কিল হয়ে যায়, পরবর্তী লঞ্চে সেটি নিরাপদ থাকতে হবে। idempotency কী এবং অনুরোধ স্টেট লোকাল স্টোরেজে রাখুন (উদাহরণ: একটি ছোট DB রো)। রিস্টার্টে একই কী নিয়ে রিট্রাই করুন বা check status endpoint‑এ সেই কী বা সার্ভার রেজাল্ট ID ব্যবহার করে স্ট্যাটাস যাচাই করুন।
সার্ভার‑সাইডে কনট্রাক্ট স্পষ্ট হওয়া উচিত: একই কী পেলে দ্বিতীয় চেষ্া প্রত্যাখ্যান করবে বা মূল রেসপন্স ফেরত দেবে (একই order ID, একই রসিদ)। যদি সার্ভার এটা এখনও করতে না পারে, ক্লায়েন্ট‑সাইড ডুপ্লিকেট প্রতিরোধ কখনো সম্পূর্ণ নির্ভরযোগ্য হবে না, কারণ অ্যাপ পাঠানোর পর কী ঘটেছে তা দেখতে পায় না।
একটি ভালো ব্যবহারকারী-ফোকাসড টাচ: যদি একটি চেষ্টা pending থাকে, দেখান “Payment in progress” এবং বাটন disable করুন যতক্ষণ না চূড়ান্ত রেজাল্ট আসে।
দুর্ঘটনাজনিত পুনরায় সাবমিট কমানোর UI প্যাটার্ন
ধীর সংযোগ শুধু অনুরোধ ভেঙে দেয় না। এটি মানুষকে ভিন্নভাবে ট্যাপ করায়। যখন স্ক্রীন দুই সেকেন্ডের জন্য ফ্রিজ করে, অনেক ব্যবহারকারী মনে করে কিছু হয়নি এবং আবার ট্যাপ করে। আপনার UI‑কে “একটি ট্যাপ” নির্ভরযোগ্য করতে হবে এমনকি নেটওয়ার্ক না থাকলে।
Optimistic UI তখনই নিরাপদ যখন অ্যাকশন reversible বা কম ঝুঁকির—যেমন কোনো আইটেমকে স্টার করা, খসড়া সংরক্ষণ, বা একটি মেসেজ রিড হিসেবে চিহ্নিত করা। অর্থ, ইনভেন্টরি, অপরিবর্তনীয় ডিলিট এবং এমন কিছু যেখানে ডুপ্লিকেট ক্ষতিকর, সেখানে Confirmed UI ভালো।
গুরুত্বপূর্ণ অ্যাকশনের জন্য একটি ভাল ডিফল্ট হলো একটি স্পষ্ট pending state। প্রথম ট্যাপে প্রাথমিক বাটনকে সঙ্গে সঙ্গে “Submitting…” স্টেটে নামিয়ে দিন, disable করুন, এবং একটি সংক্ষিপ্ত লাইন দেখান যা চলছে কি হচ্ছে।
ফ্লোকি নেটওয়ার্কে কাজ করা ভাল কিছু প্যাটার্ন:
- ট্যাপের পরে প্রাইমারি অ্যাকশন disable করুন এবং চূড়ান্ত রেজাল্ট না পাওয়া পর্যন্ত disable রাখুন।
- একটি দৃশ্যমান “Pending” স্ট্যাটাস দেখান (পরিমাণ, রিসিভার, আইটেম কাউন্ট ইত্যাদি)।
- ব্যবহারকারীরা কি পাঠিয়েছে তা নিশ্চিত করার জন্য একটি “Recent activity” ভিউ যোগ করুন।
- যদি অ্যাপ ব্যাকগ্রাউন্ড হয়, ফিরে এলে pending স্ট্যাটাস রাখুন।
- একই স্ক্রীনে একাধিক ট্যাপ টার্গেটের পরিবর্তে একটি স্পষ্ট প্রাইমারি বাটন রাখুন।
কখনও কখনও অনুরোধ সফল হয় কিন্তু রেসপন্স হারিয়ে যায়। এটাকে একটি নর্মাল আউটকাম হিসেবেই দেখুন—এটাকে এমন ত্রুটিতে পরিণত করবেন না যা পুনরায় ট্যাপকে আমন্ত্রণ করে। “Failed, try again” বলার বদলে “We’re not sure yet” দেখান এবং নিরাপদ পরবর্তী পদক্ষেপ দিন যেমন “স্ট্যাটাস চেক করুন।” যদি আপনি স্ট্যাটাস চেক করতে না পারেন, লোকাল pending রেকর্ড রাখুন এবং ব্যবহারকারীকে জানান আপনি কানেকশন ফিরে এলে আপডেট দেবেন।
“Try again” স্পষ্ট ও নিরাপদ করুন—শুধু তখন দেখান যখন আপনি একই ক্লায়েন্ট‑সাইড অনুরোধ ID বা idempotency কী ব্যবহার করে পুনরাবৃত্তি করতে পারবেন।
বাস্তব উদাহরণ: ফ্লোকি চেকআউট সাবমিশন
একজন গ্রাহক ট্রেনে স্পট্টি সিগন্যাল নিয়ে আছেন। তারা কার্টে আইটেম যোগ করে Pay ট্যাপ করে। অ্যাপকে ধৈর্য ধরতে হবে, কিন্তু একই সাথে দুইটি অর্ডার তৈরি করা থেকে রক্ষা করতে হবে।
নিরাপদ সিকোয়েন্স দেখতে এমন হবে:
- অ্যাপ একটি ক্লায়েন্ট‑সাইড চেষ্টা ID তৈরি করে এবং একটি idempotency কী (উদাহরণ UUID) দিয়ে চেকআউট অনুরোধ পাঠায়।
- অনুরোধ একটি স্পষ্ট connect timeout ও পরে একটি দীর্ঘ রিড টাইমআউটের জন্য অপেক্ষা করে। ট্রেন টানেলে যায়, এবং কলটি টাইমআউট করে।
- অ্যাপ কেবল তখনই একবার রিট্রাই করে, এবং কেবল তখনই যদি এটি কখনই সার্ভার উত্তর পায়নি।
- সার্ভার দ্বিতীয় অনুরোধ পেলে একই idempotency কী দেখে মূল রেজাল্ট ফেরত দেয়, নতুন অর্ডার তৈরি করে না।
- অ্যাপ সফল রেসপন্স পেলে চূড়ান্ত কনফার্মেশন স্ক্রীন দেখায়, এমনকি সেটি রিট্রাই থেকে এসেছে—প্রায়ই ব্যবহারকারী সেটা জানবে না।
ক্যাশিং কঠোর নিয়ম মেনে চলে। প্রোডাক্ট লিস্ট, ডেলিভারি অপশন এবং ট্যাক্স টেবিলগুলো স্বল্পকালীন ক্যাশ করা যায় (GET)। চেকআউট সাবমিশন (POST) কখনই ক্যাশ করবেন না। HTTP ক্যাশ থাকলেও এটি ব্রাউজিংয়ে সাহায্য করবে, পেমেন্ট “মনে রাখে” এমনভাবে নয়।
ডুপ্লিকেট প্রতিরোধ নেটওয়ার্ক ও UI সিদ্ধান্তের মিশ্রণ। ব্যবহারকারী Pay ট্যাপ করলে বাটন disable হয়ে যায় এবং স্ক্রীনে “Submitting order...” দেখায়, কেবলএকটি Cancel অপশন। নেটওয়ার্ক হারালে “Still trying” দেখায় এবং একই চেষ্টা ID রাখে। ব্যবহারকারী যদি ফোর্স‑ক্লোজ করে পুনরায় খোলে, অ্যাপ সেই ID ব্যবহার করে অর্ডার স্ট্যাটাস চেক করে পুনরায় টাকা না চাওয়াই সেরা।
দ্রুত চেকলিস্ট ও পরবর্তী ধাপ
আপনার অ্যাপ অফিস Wi‑Fi‑তে “মোটামুটি ঠিক” কিন্তু ট্রেন, লিফট বা গ্রামীণ এলাকায় ভেঙে পড়ে যদি, তাহলে এটাকে একটি রিলিজ গেট হিসেবে বিবেচনা করুন। এই কাজটি জটিল কোডের চাইতে স্পষ্ট নিয়ম নিয়ে—যা বারবার করা যায়—আরও বেশি সম্পর্কিত।
শিপ করার আগে চেকলিস্ট:
- প্রতিটি endpoint টাইপের জন্য টাইমআউট নির্ধারণ করুন (লগইন, ফিড, আপলোড, চেকআউট) এবং throttled ও উচ্চ‑লেটেন্সি নেটওয়ার্কে টেস্ট করুন।
- যেখানে নিরাপদ তা ছাড়া রিট্রাই করবেন না, এবং ব্যাকঅফ দিয়ে সেটি সীমাবদ্ধ করুন (পড়ার জন্য কয়েকটি চেষ্টা, লেখার জন্য সাধারণত না)।
- প্রতিটি গুরুত্বপূর্ণ লেখার জন্য idempotency কী যোগ করুন (পেমেন্ট, অর্ডার, ফর্ম) যাতে রিট্রাই বা ডবল ট্যাপ ডুপ্লিকেট তৈরি করতে না পারে।
- ক্যাশিং নিয়ম স্পষ্ট করুন: কী স্টেল হতে পারে, কী অবশ্যই তাজা হবে, এবং কী কখনই ক্যাশ করা যাবে না।
- স্টেট দৃশ্যমান করুন: pending, failed এবং completed আলাদা দেখান, এবং অ্যাপ রিস্টার্টের পরে completed পদক্ষেপ মনে রাখে।
যদি এর মধ্যে কোনোটি “পরে ঠিক করা হবে” থাকে, আপনাকে স্ক্রিন জুড়ে এলোমেলো আচরণ পাবেন।
সেটি কার্যকর করতে পরবর্তী ধাপ
এক পাতার নেটওয়ার্ক নীতি লিখুন: endpoint ক্যাটাগরি, টাইমআউট লক্ষ্য, রিট্রাই নিয়ম, এবং ক্যাশিং প্রত্যাশা। এটি এক জায়গায় (interceptors, shared client factory, বা একটি ছোট wrapper) এনফোর্স করুন যাতে প্রতিটি টিম মেম্বার ডিফল্টভাবে একই আচরণ পায়।
তারপর একটি ছোট ডুপ্লিকেট ড্রিল করুন। একটি গুরুত্বপূর্ণ অ্যাকশন (যেমন চেকআউট) নিন, স্পিনার ফ্রিজ এমুলেট করুন, অ্যাপ ফোর্স‑ক্লোজ করুন, এয়ারপ্লেন মোড টগল করুন, এবং আবার বাটন চাপুন। যদি আপনি প্রমাণ করতে না পারেন এটা নিরাপদ, ব্যবহারকারীরা একদিন করে ভাঙবেই।
যদি আপনি একই নিয়ম ব্যাকএন্ড ও ক্লায়েন্ট জুড়ে প্রয়োগ করতে চান কিন্তু সবকিছু হাতে‑হাতে ওয়্যারিং করতে না চান, AppMaster (appmaster.io) উৎপাদন-রেডি ব্যাকএন্ড এবং নেটিভ মোবাইল সোর্স কোড জেনারেট করতে সাহায্য করতে পারে। তবুও মূল বিষয় হলো নীতি: idempotency, রিট্রাই, ক্যাশিং, এবং UI স্টেট একবার নির্ধারণ করুন এবং পুরো ফ্লো জুড়ে একরকমভাবে প্রয়োগ করুন।
প্রশ্নোত্তর
শুরুর আগে প্রতিটি স্ক্রীন ও অ্যাকশনের জন্য “সঠিক” কী তা সংজ্ঞায়িত করুন—বিশেষ করে যেগুলো একবারের বেশি ঘটে চললে সমস্যা হবে, যেমন পেমেন্ট বা অর্ডার। নিয়মগুলি স্পষ্ট হলে টাইমআউট, রিট্রাই, ক্যাশিং এবং UI স্টেট সেট করা সহজ হয়।
ব্যবহারকারীরা সাধারণত সীমাহীন স্পিনার, দীর্ঘ অপেক্ষার পরে ত্রুটি, দ্বিতীয় চেষ্টায় কাজ করা, বা ডুপ্লিকেট ফলাফল (দুইটি অর্ডার বা দ্বিগুণ চার্জ) দেখতে পান। এসবের প্রধান কারণ হচ্ছে অনির্ধারিত রিট্রাই এবং ‘পেন্ডিং বনাম ফেইলড’ নিয়মের অনিশ্চয়তা।
কনেক্ট টাইমআউট হলো সার্ভারের সঙ্গে সংযোগ নির্মাণের জন্য অপেক্ষার সময়, রাইট টাইমআউট হলো রিকুয়েস্ট বডি পাঠানোর জন্য, আর রিড টাইমআউট হলো রিকুয়েস্ট পাঠানোর পরে সার্ভারের রেসপন্সের জন্য অপেক্ষা। কম রিস্কের পড়ার কাজগুলোর জন্য সংক্ষিপ্ত টাইমআউট, এবং গুরুত্বপূর্ণ সাবমিশনের জন্য দীর্ঘতর রিড/রাইট টাইমআউট ব্যবহার করুন—সাথেই UI-তে একটানা অপেক্ষার সীমা দেখান।
হ্যাঁ—যদি কেবল একটি টাইমআউট সেট করতে পারেন, callTimeout ব্যবহার করুন যাতে পুরো অপারেশন-এন্ড-টু-এন্ড সীমাবদ্ধ থাকে এবং “অন্তহীন” অপেক্ষা এড়ানো যায়। তারপর প্রয়োজনে connect/read/write আলাদাভাবে লেয়ার করুন।
স্বল্পস্থায়ী সমস্যাগুলো (কনেকশন ড্রপ, DNS সমস্যা, টাইমআউট) এবং মাঝে মাঝে 502/503/504 ডিফল্টভাবে রিট্রাই করা যায়। 4xx ত্রুটিগুলো (খারাপ অনুরোধ, অ্যানথরাইজেশন ইত্যাদি) রিট্রাই করা উচিত নয়; 408 বা 429 ছাড়া 4xx-এ স্বয়ংক্রিয় রিট্রাই এড়ান।
কম সংখ্যক রিট্রাই (সাধারণত 1–3) ব্যবহার করুন, এক্সপোনেনশিয়াল ব্যাকঅফের সঙ্গে ছোট র্যান্ডম জিটার দিন যাতে অনেক ডিভাইস একই সময়ে রিট্রাই না করে। মোট রিট্রাইয়ের উপর একটি সময়সীমা রাখুন (উদাহরণ: 10–20 সেকেন্ড) যাতে ব্যবহারকারী দীর্ঘ স্পিনারে আটকে না পড়ে।
আইডেমপোটেন্সি মানে একই অনুরোধ বারবার করলে দ্বিতীয়বার নতুন কিছু তৈরি হবে না—এটি প্রথম ফলাফলই ফেরত দেয়। পেমেন্ট ও অর্ডারগুলোর মতো গুরুত্বপূর্ণ কাজের জন্য প্রতিটি চেষ্টা আলাদা idempotency কী জেনারেট করে পাঠান এবং রিট্রাইতে সেই একই কী ব্যবহার করুন যাতে ডবল চার্জ বা ডবল বুকিং না হয়।
ব্যবহারকারী অ্যাকশন শুরু করলে একটি ইউনিক কী জেনারেট করুন, লোকালি একটি ছোট “pending” রেকর্ডে সংরক্ষণ করুন, এবং অনুরোধের সঙ্গে কীটি পাঠান। রিস্টার্ট বা রিট্রাই হলে সেই একই কী পুনরায় ব্যবহার করুন অথবা স্ট্যাটাস চেক করে নিশ্চিত করুন—এভাবে একই ব্যবহারকারীর ইচ্ছাকে দুইটি সার্ভার লিখন বানানো যাবে না।
শুধু এমন ডেটাই ক্যাশ করুন যেটা কিছুটা পুরনো হলেও ব্যবহারকারী সহ্য করতে পারে; অর্থ, নিরাপত্তা বা চূড়ান্ত সিদ্ধান্ত প্রভাবিত করে এমন কিছু কখনও পুরনো ক্যাশ থেকে দেখাবেন না। পড়া (GET) এ ETag ভিত্তিক ভ্যালিডেশন ভালো কাজ করে; লেখা (POST/PUT/PATCH/DELETE) কখনই ক্যাশ করবেন না এবং সংবেদনশীল রেসপন্সে no-store ব্যবহার করুন।
প্রাইমারি বাটন ট্যাপ করার পর এটিকে অবিলম্বে disable করে “Submitting…” স্টেট দেখান এবং একটি দৃশ্যমান pending স্ট্যাটাস রাখুন যা ব্যাকগ্রাউন্ডিং বা রিস্টার্টের পরে রয়ে যায়। রেসপন্স হারিয়ে গেলে ব্যবহারকারীকে “We’re not sure yet” বা “স্ট্যাটাস চেক করুন” এর মত নিরাপদ বিকল্প দেখান, যাতে তারা বারবার ট্যাপ না করে।


