ব্লগে ফিরে যান
১৩ আগস্ট, ২০২৬Sergei Solod17 মিনিট পড়া

ডিপ্লয়মেন্টের পরে পুরোনো Next.js ট্যাব কেন ভেঙে যায়: পুরোনো HTML, হারিয়ে যাওয়া চাঙ্ক ও সংস্করণ-অসামঞ্জস্য

একটি ডিপ্লয়মেন্টের পরে প্রোডাকশন মনিটরিংয়ে আমার নিজের Next.js অ্যাপ্লিকেশনের একটি JavaScript চাঙ্ক লোড হতে ব্যর্থ হয়। লগটি ব্যর্থতা প্রমাণ করেছিল, কারণ নয়। সেই ঘটনাকে ভিত্তি করে এই লেখায় পুরোনো ট্যাব, পুরোনো HTML, হারিয়ে যাওয়া /_next/static অ্যাসেট, সংস্করণ-অসামঞ্জস্য, পুরোনো সম্পদ সংরক্ষণের সময়সীমা, deploymentId, নিরাপদ ধাপে ধাপে প্রকাশ, মনিটরিং ও নিয়ন্ত্রিত পুনরুদ্ধার ব্যাখ্যা করা হয়েছে।

Next.jsডিপ্লয়মেন্টসংস্করণ-অসামঞ্জস্যওয়েব ক্যাশিংফ্রন্টএন্ড নির্ভরযোগ্যতাস্থির ওয়েব সম্পদ

একটি ডিপ্লয়মেন্টের পরে প্রোডাকশনে দেখা সবচেয়ে কাজে লাগা ত্রুটিগুলোর একটি প্রথমে একেবারেই সাধারণ মনে হয়েছিল:

Failed to load script:
/_next/static/chunks/9253.647385b4be0958e4.js

একই ত্রুটি-স্ট্রিমে অ্যানালিটিক্সের ব্যর্থতা, বিজ্ঞাপনের স্ক্রিপ্ট, সাধারণ Script error. বার্তা এবং মাঝপথে থেমে যাওয়া ভিডিও প্লেব্যাকও ছিল। এর বেশিরভাগই ছিল মনিটরিংয়ের শব্দ। এই ঘটনাটি আলাদা ছিল: যে রিসোর্সটি লোড হয়নি, সেটি আমার নিজের Next.js অ্যাপ্লিকেশনের অংশ। ব্রাউজার যদি সত্যিই সেটি আনতে না পারে, পেজের কোনো অংশ কাজ করা বন্ধ করতে পারে।

তবে লগটি ব্যর্থতার কারণ প্রমাণ করছিল না। সাময়িক নেটওয়ার্ক সমস্যা হতে পারে, প্রক্সি বা CDN ব্যর্থ হতে পারে, ফাইলটি সত্যিই অনুপস্থিত হতে পারে, অথবা পুরোনো কোনো পেজ আগের ডিপ্লয়মেন্টের চাঙ্ক চাইতে পারে যখন সার্ভার ইতিমধ্যে নতুন বিল্ডে চলে গেছে।

শেষের ঘটনাটি সহজেই চোখ এড়ায়, কারণ নতুন ডিপ্লয়মেন্ট পুরোপুরি সুস্থ হতে পারে। নতুন ভিজিটররা বর্তমান সংস্করণ পায় এবং সব ঠিক চলে, অথচ কয়েক ঘণ্টা ধরে খোলা একটি ট্যাব বাস্তবে এখনও আগের রিলিজের ক্লায়েন্ট হয়ে থাকে।

এই লেখার বিষয় সেই সামঞ্জস্যের ফাঁকটি: ডিপ্লয়মেন্টের পরে পুরোনো Next.js ট্যাব কেন ভেঙে যেতে পারে, পুরোনো HTML এবং মুছে ফেলা /_next/static অ্যাসেট কীভাবে সংস্করণ-অসামঞ্জস্য তৈরি করে, অতিরিক্ত আক্রমণাত্মক পরিষ্কার কেন সমস্যা বাড়ায়, আর রিটেনশন, ডিপ্লয়মেন্টের ক্রম, মনিটরিং ও রিকভারি কীভাবে সাজালে সফল রিলিজ আগে থেকেই খোলা সেশনগুলোকে ভেঙে দেবে না।

প্রথম শিক্ষা: প্রতিটি স্ক্রিপ্ট ব্যর্থতাই ডিপ্লয়মেন্টের সমস্যা নয়

মূল ত্রুটি-স্ট্রিমে একেবারে ভিন্ন ধরনের কয়েকটি সমস্যা একসঙ্গে ছিল। তৃতীয় পক্ষের অ্যানালিটিক্স বা বিজ্ঞাপনের স্ক্রিপ্ট কনটেন্ট ব্লকার, DNS ফিল্টার, ব্রাউজারের গোপনীয়তা-ফিচার, অ্যান্টিভাইরাস, আঞ্চলিক ফিল্টারিং বা ব্যবহারকারীর নেটওয়ার্কের কারণে আটকে যেতে পারে। ভিডিওর play() প্রতিশ্রুতি পরে pause() ডাকা হলে বাতিল হতে পারে, অথচ অ্যাপ্লিকেশন মোটেও ভাঙা নয়। আর ক্রস-অরিজিন Script error. বার্তা প্রায়ই রোগ নির্ণয়ের মতো তথ্যই দেয় না।

কিন্তু নিজের Next.js চাঙ্ক লোড না হওয়া আলাদা অগ্রাধিকার পাওয়ার মতো ঘটনা। কাজে লাগা বিভাজনটি বরং এমন:

তৃতীয় পক্ষের রিসোর্স ব্যর্থ
    -> সাধারণত টেলিমেট্রি বা ঐচ্ছিক ফিচার ক্ষতিগ্রস্ত

নিজস্ব /_next/static/*.js ব্যর্থ
    -> অ্যাপ্লিকেশনের প্রয়োজনীয় কোড অনুপস্থিত হতে পারে

এই পার্থক্য গুরুত্বপূর্ণ, কারণ অতিরিক্ত সংবেদনশীল রিপোর্টার শত শত অপ্রাসঙ্গিক বার্তার মধ্যে আসল ঘটনাকে ঢেকে দিতে পারে। আমার ক্ষেত্রে নজর দেওয়ার মতো রিসোর্স ছিল /_next/static/chunks/9253.647385b4be0958e4.js। লগটি প্রমাণ করেছিল যে নিজের একটি স্ক্রিপ্ট লোড হয়নি। কিন্তু এটি প্রমাণ করেনি যে ডিপ্লয়মেন্ট-পরবর্তী সংস্করণ-অসামঞ্জস্যই কারণ ছিল।

আমি এই প্রমাণের সীমাটি ইচ্ছাকৃতভাবে স্পষ্ট রাখি: সম্ভাব্য কারণ আর প্রমাণিত কারণ এক জিনিস নয়।

খোলা ব্রাউজার ট্যাব আসলে পুরোনো রিলিজের একটি ক্লায়েন্ট

যে মানসিক মডেলটি সমস্যাটি আমার কাছে পরিষ্কার করেছে, সেটি সহজ: ডিপ্লয়মেন্টের পরে অ্যাপ্লিকেশনের একাধিক সংস্করণ একই সময়ে চালু থাকতে পারে।

ধরা যাক রিলিজ A সকাল ১০টায় লাইভ। একজন ব্যবহারকারী একটি পেজ খুলে সেই রুটের HTML ও JavaScript পায়। ১০:৩০-এ রিলিজ B প্রকাশিত হয়। নতুন ভিজিটররা B পায়। কিন্তু আগে থেকেই খোলা ট্যাবটি শুধু সার্ভার বদলেছে বলে স্বয়ংক্রিয়ভাবে B হয়ে যায় না।

সেই ট্যাবে এখনও থাকতে পারে:

  • রিলিজ A-এর JavaScript রানটাইম;
  • A তৈরি করা রুট ও চাঙ্কের রেফারেন্স;
  • A থেকে আগে থেকে আনা নেভিগেশন ডেটা;
  • A চলার সময় তৈরি React স্টেট;
  • A থেকে ইতিমধ্যে ডাউনলোড হওয়া কোড-স্প্লিট মডিউল;
  • A-এর এমন মডিউলের রেফারেন্স, যা এখনও ডাউনলোড হয়নি।

শেষের বিষয়টিই সাধারণত ব্যর্থতাকে দৃশ্যমান করে।

ব্যবহারকারীর ভবিষ্যতে লাগবে এমন সব চাঙ্ক যদি আগে থেকেই ব্রাউজার ক্যাশে থাকে, সেশন কোনো সমস্যা ছাড়াই চলতে পারে। কিন্তু Next.js কোড ভাগ করে। রুট বদলানো, dynamic import, মডাল, এডিটর বা পরে ব্যবহৃত কোনো ফিচারের জন্য নতুন JavaScript ফাইল লাগতে পারে। তখন পুরোনো রানটাইম এমন একটি URL চায়, যা রিলিজ A-তে বৈধ ছিল।

সার্ভার যদি অ্যাসেটটি এখনও রেখে দেয়, সব স্বাভাবিক চলতে পারে। ডিপ্লয়মেন্ট যদি সেটি মুছে দেয়, পুরোনো ক্লায়েন্ট 404 পেতে পারে, যদিও নতুন ব্যবহারকারীদের জন্য রিলিজ B পুরোপুরি সুস্থ।

কনটেন্ট-হ্যাশযুক্ত চাঙ্ক দীর্ঘমেয়াদি ক্যাশের জন্যই তৈরি

Next.js সত্যিকারের অপরিবর্তনীয় অ্যাসেটকে ইচ্ছাকৃতভাবে দীর্ঘমেয়াদি ক্যাশ নীতি দেয়। বর্তমান নিজস্ব হোস্টিং ডকুমেন্টেশন অনুযায়ী, নামের মধ্যে SHA হ্যাশ থাকা অপরিবর্তনীয় অ্যাসেট এমন নীতিতে সার্ভ করা হয়:

Cache-Control: public, max-age=31536000, immutable

এটি যৌক্তিক: কনটেন্ট বদলালে URL বদলে যায়। তাই ব্রাউজারকে প্রতিটি রিকোয়েস্টে একই ফাইল আবার যাচাই করতে হয় না।

এর গুরুত্বপূর্ণ ফল হলো: যতক্ষণ কোনো পুরোনো ডকুমেন্ট বা রানটাইম পুরোনো URL-টি উল্লেখ করতে পারে, ততক্ষণ URL-টির অর্থ আছে।

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

তাই “আমাদের স্থির ফাইলগুলো অপরিবর্তনীয়” আর “আগের রিলিজের স্থির ফাইলগুলো সঙ্গে সঙ্গে মুছে ফেলা যায়” একই কথা নয়। অপরিবর্তনীয়তা পুরোনো অ্যাসেট নিরাপদে রেখে দিতে সাহায্য করে। এটি পুরোনো ক্লায়েন্টকে সেগুলো চাইতে বাধা দেয় না।

বর্তমান Next.js-এর নিজস্ব হোস্টিং নির্দেশিকা বহু-সার্ভার বা ধাপে ধাপে ডিপ্লয়মেন্টে JavaScript বা CSS অ্যাসেট অনুপস্থিত হওয়াকে সংস্করণ-অসামঞ্জস্যের একটি লক্ষণ হিসেবে উল্লেখ করে। একই ধরনের সমস্যা পুরোনো ট্যাব ও ইতিমধ্যে আপডেট হওয়া অরিজিনের মাঝেও ঘটতে পারে।

সংস্করণ কয়েকটি ভিন্ন পথে আলাদা হয়ে যেতে পারে

“ক্যাশের সমস্যা” বলা রোগ নির্ণয়ের জন্য খুবই অস্পষ্ট। আমি অন্তত চারটি আলাদা প্রক্রিয়া দেখি, কারণ প্রত্যেকটির সমাধানও আলাদা।

1. পুরোনো ট্যাব এমন অ্যাসেট চায়, যা ডিপ্লয়মেন্টের আগে লোড হয়নি

এটি দীর্ঘসময় খোলা সেশনের পরিচিত ঘটনা। ডকুমেন্ট ও রানটাইম A থেকে এসেছে। B সার্ভারের ফাইল বদলে দেয়। পরে ব্যবহারকারী এমন ফিচার খোলেন, যার জন্য A-এর বিলম্বে লোড হওয়া চাঙ্ক দরকার। সেই চাঙ্ক মুছে গেলে রিকোয়েস্ট ব্যর্থ হয়।

2. পুরোনো HTML এমন চাঙ্কের দিকে নির্দেশ করে, যা আর নেই

CDN, রিভার্স প্রক্সি, Service Worker, ব্রাউজার ক্যাশ বা স্থির হোস্টিং স্তর প্রত্যাশার চেয়ে বেশি সময় পুরোনো HTML ধরে রাখতে পারে। HTML এখনও A-এর অ্যাসেট উল্লেখ করে, অথচ অরিজিনে শুধু B আছে।

HTML-কে ভুল করে হ্যাশযুক্ত অ্যাসেটের মতো দীর্ঘ immutable নীতি দিলে ঝুঁকি আরও বাড়ে। চাঙ্ক দীর্ঘ সময় ক্যাশ করা যায়, কারণ তার URL-ই কনটেন্টের সংস্করণ নির্ধারণ করে। HTML-ই বরং ঠিক করে কোন কোন চাঙ্ক URL একসঙ্গে একটি সামঞ্জস্যপূর্ণ রিলিজ তৈরি করে।

3. ধাপে ধাপে বা বহু-ইনস্ট্যান্স ডিপ্লয়মেন্ট রিলিজ মিশিয়ে ফেলে

লোড ব্যালান্সারের পেছনে দুটি Next.js ইনস্ট্যান্স ভাবুন। একটি B-তে, অন্যটি এখনও A-তে। ডকুমেন্ট এক সংস্করণ থেকে আসতে পারে, আর পরের নেভিগেশন অন্য সংস্করণে পৌঁছাতে পারে। বর্তমান Next.js ডকুমেন্টেশন এটিকে সংস্করণ-অসামঞ্জস্য বলে এবং অনুপস্থিত অ্যাসেট, সার্ভার ফাংশন-এর অসামঞ্জস্য ও নেভিগেশন ব্যর্থতাকে সম্ভাব্য ফল হিসেবে উল্লেখ করে।

নিরাপদ ভিত্তি হলো একটি যৌক্তিক রিলিজ একবার বিল্ড করা এবং সেই একই বিল্ড আর্টিফ্যাক্ট সব ইনস্ট্যান্সে চালানো। Next.js নিজস্ব হোস্টিং ডকুমেন্টেশনও একই ডিপ্লয়মেন্টের কনটেইনারগুলোতে একই বিল্ড ও সামঞ্জস্যপূর্ণ বিল্ড আইডি ব্যবহারের পরামর্শ দেয়, প্রতিটি রেপ্লিকায় আলাদা বিল্ড নয়।

4. ডিপ্লয়মেন্ট ফাইলগুলো ভুল ক্রমে প্রকাশ করে

পুরোনো ট্যাব না থাকলেও অ-অ্যাটমিক আপলোড সাময়িকভাবে অসম্ভব একটি অবস্থা তৈরি করতে পারে:

নতুন HTML দেখা যাচ্ছে
+
নতুন চাঙ্ক এখনও পাওয়া যাচ্ছে না

অথবা উল্টোটা:

পুরোনো HTML এখনও দেখা যাচ্ছে
+
পুরোনো চাঙ্ক ইতিমধ্যে মুছে গেছে

খুব ছোট সময়ের জানালাও যথেষ্ট। ব্যবহারকারীকে শুধু একবার ভুল মুহূর্তে সেখানে পড়তে হবে।

বিপজ্জনক প্যাটার্ন: সবকিছু বদলে পুরোনো ট্রি মুছে ফেলা

সহজ ডিপ্লয়মেন্ট স্ক্রিপ্ট প্রায়ই এমন কিছু দিয়ে শুরু হয়:

build
rsync --delete new-output/ production/
restart

এটি আকর্ষণীয়, কারণ প্রোডাকশন ডিরেক্টরি সবসময় সর্বশেষ বিল্ডের সঙ্গে হুবহু মেলে। কিন্তু দীর্ঘসময় বেঁচে থাকা ক্লায়েন্টের জন্য এই প্যাটার্ন ক্ষতিকর।

হ্যাশযুক্ত অ্যাসেটে পুরোনো ফাইল সাধারণত নতুন ফাইলের সঙ্গে সংঘর্ষ করে না, কারণ URL আলাদা। সঙ্গে সঙ্গে মুছে দিলে মূলত ডিস্কের জায়গা বাঁচে। বিনিময়ে পুরোনো ক্লায়েন্টের এখনও বৈধ প্রতিটি রেফারেন্স সম্ভাব্য 404 হয়ে যায়।

আমি এখন পুরোনো চাঙ্ককে ডিপ্লয়মেন্ট সামঞ্জস্যের অংশ হিসেবে দেখি, আবর্জনা হিসেবে নয়।

তার মানে প্রতিটি রিলিজ চিরকাল রাখা নয়। পরিষ্কার করা আলাদা রিটেনশন নীতি হওয়া উচিত; নতুন রিলিজ প্রকাশের পার্শ্বপ্রতিক্রিয়া নয়।

রিটেনশন কাজে দেয়, কিন্তু কোনো সীমিত সময়সীমাই পূর্ণ সমাধান নয়

নিজস্ব হোস্টিংয়ে সেটআপে পুরোনো /_next/static অ্যাসেট একটি গ্রেস পিরিয়ড পর্যন্ত রাখা যায়। সঠিক সময়সীমা ব্যবহারের ধরন অনুযায়ী বদলায়। যে সাইটে ব্যবহারকারী দুই মিনিট পড়ে চলে যান, সেটির ঝুঁকি এমন অ্যাপের মতো নয় যা সারাদিন খোলা থাকে।

ন্যূনতম সময়সীমা নিয়ে ভাবার একটি উপায়:

রিটেনশন সময়সীমা >=
    পুরোনো HTML-এর প্রত্যাশিত স্থায়িত্ব
    + দীর্ঘসময় খোলা ট্যাবের বাস্তবসম্মত সময়
    + রোলব্যাকের সময়সীমা
    + ডিপ্লয়মেন্ট ছড়িয়ে পড়ার অতিরিক্ত সময়

এটি গাণিতিক নিশ্চয়তা নয়। একটি ট্যাব সপ্তাহের পর সপ্তাহ খোলা থাকতে পারে। কোনো সীমিত সংখ্যক ঘণ্টা এই ধরনের ব্যর্থতাকে অসম্ভব করে না।

তাই আমি কয়েকটি স্তর একসঙ্গে ব্যবহার করতে চাই:

  1. আগের অপরিবর্তনীয় অ্যাসেট এতদিন রাখুন, যাতে স্বাভাবিক পুরোনো সেশন চলতে পারে;
  2. সংস্করণ-অসামঞ্জস্য শনাক্ত করুন, যাতে ক্লায়েন্ট বর্তমান রিলিজে যেতে পারে;
  3. অ্যাসেট সত্যিই অনুপস্থিত হলে নিরাপদ, একবারের রিলোড বা দৃশ্যমান রিকভারি পথ রাখুন;
  4. নিজস্ব চাঙ্ক অনুপস্থিত হওয়া মনিটর করুন, যাতে বাস্তব তথ্য থেকে রিটেনশন সমন্বয় করা যায়।

রিটেনশন অধিকাংশ ব্যর্থতা ঠেকায়। রিকভারি সেই দীর্ঘ লেজটি সামলায়, যা কোনো সীমিত রিটেনশন সময় পুরোপুরি মুছে দিতে পারে না।

শুধু ফাইলের বয়স দেখে পুরোনো চাঙ্ক মুছবেন না

“সাত দিনের বেশি পুরোনো সব ফাইল মুছে দাও”—এমন নিয়মও ভুল হতে পারে। বর্তমান রিলিজ এমন একটি পুরোনো হ্যাশযুক্ত ফাইল আবার ব্যবহার করতে পারে যার কনটেন্ট বদলায়নি; ফলে ডিস্কে তার পরিবর্তনের সময় পুরোনো হলেও ফাইলটি এখনও দরকার।

আরও নির্ভরযোগ্য গারবেজ কালেকশন রিলিজ সম্পর্কে সচেতন হবে:

  1. সামঞ্জস্যের উইন্ডোর মধ্যে থাকা সব রিলিজের ম্যানিফেস্ট বা অ্যাসেট তালিকা রাখুন;
  2. সেসব রিলিজে উল্লেখ করা সব পথের সমষ্টি তৈরি করুন;
  3. সুরক্ষিত ওই সেটের কোনো ফাইল মুছবেন না;
  4. আর কোথাও উল্লেখ নেই এমন অ্যাসেট অতিরিক্ত গ্রেস পিরিয়ডের পরেই মুছুন।

ছোট ডিপ্লয়মেন্টের জন্য এই ব্যবস্থা অতিরিক্ত জটিল হলে, একটু বড় স্থির-অ্যাসেট ডিরেক্টরি রাখা বিরল প্রোডাকশন ত্রুটি খুঁজে বেড়ানোর চেয়ে অনেক সময় সস্তা।

আমি যে নিয়মটি এড়াই, সেটি সরল: শেয়ার করা /_next/static ট্রিতে --delete যেন নতুন রিলিজ চালু করার একই ধাপের অংশ না হয়।

Next.js এখন সংস্করণ-অসামঞ্জস্য সামলাতে পারে, কিন্তু পুরোনো অ্যাসেট সংরক্ষণ করে না

বর্তমান Next.js-এ সংস্করণ-অসামঞ্জস্য সামলানোর জন্য deploymentId সমর্থন রয়েছে। কনফিগারেশন এমন হতে পারে:

// next.config.js
const nextConfig = {
  deploymentId: process.env.DEPLOYMENT_VERSION,
}

module.exports = nextConfig

বর্তমান deploymentId ডকুমেন্টেশন অনুযায়ী, Next.js ফ্রেমওয়ার্ক-পরিচালিত স্থির অ্যাসেট URL-এ ?dpl=<deploymentId> যোগ করে, ক্লায়েন্ট-সাইড নেভিগেশন রিকোয়েস্টে x-deployment-id পাঠায়, নেভিগেশন রেসপন্সে x-nextjs-deployment-id দেয় এবং <html>-এ data-dpl-id বসায়। নেভিগেশনের সময় আইডি না মিললে অসামঞ্জস্যপূর্ণ আংশিক নেভিগেশন চালিয়ে যাওয়ার বদলে সম্পূর্ণ নেভিগেশন করা যায়।

?dpl=<deploymentId>
x-deployment-id
x-nextjs-deployment-id
data-dpl-id

এটি দরকারি, কিন্তু ফিচারটির ক্ষমতা বাড়িয়ে ধরা ঠিক নয়। ডকুমেন্টেশন স্পষ্ট বলে যে আগত ?dpl= প্যারামিটার Next.js পুরোনো ডিপ্লয়মেন্টে রিকোয়েস্ট রুট করার জন্য পড়ে না; এটি ক্যাশ-বাস্টিংয়ের জন্য। নিজস্ব হোস্টিংয়ে অরিজিন যদি পুরোনো ফাইলটি সত্যিই মুছে দেয়, কুয়েরি প্যারামিটার সেটি ফিরিয়ে আনতে পারে না।

তাই আমি deploymentId-কে অসামঞ্জস্য শনাক্ত ও রিকভারি করার ব্যবস্থা হিসেবে দেখি, অ্যাসেট রিটেনশন বা শৃঙ্খলাপূর্ণ ডিপ্লয়মেন্টের বিকল্প হিসেবে নয়।

কিছু হোস্টিং প্ল্যাটফর্ম আরও এগিয়ে যায়। উদাহরণ হিসেবে Vercel-এর সংস্করণ-অসামঞ্জস্য সুরক্ষা নির্দেশিকা সংস্করণ স্থিরকরণ বর্ণনা করে, যাতে ফ্রেমওয়ার্ক-পরিচালিত রিকোয়েস্ট সেই ডিপ্লয়মেন্টেই যেতে পারে যেটি ক্লায়েন্টকে প্রথম পেজটি দিয়েছিল। এটি প্ল্যাটফর্মের সক্ষমতা; যেকোনো Nginx বা CDN সেটআপে এটি আছে ধরে নেওয়া যায় না।

বিল্ড আইডি আর ডিপ্লয়মেন্ট আইডি সম্পর্কিত, কিন্তু এক জিনিস নয়

Next.js next build-এর সময় একটি বিল্ড আইডি-ও তৈরি করে। একই ডিপ্লয়মেন্ট সার্ভ করার কথা এমন একাধিক কনটেইনার যদি আলাদা আলাদা বিল্ড চালায়, সেগুলো চুপচাপ ভিন্ন বিল্ডে পরিণত হওয়া উচিত নয়।

নির্ধারিত বিল্ড আইডি-কে Git কমিট-এর মতো রিলিজ পরিচয়ের সঙ্গে যুক্ত করা যায়:

// next.config.js
const nextConfig = {
  generateBuildId: async () => process.env.GIT_SHA,
  deploymentId: process.env.GIT_SHA,
}

module.exports = nextConfig

এই উদাহরণটি শুধু ব্যাখ্যার জন্য; আমার প্রোডাকশন কোড থেকে নেওয়া নয়। মূল স্থাপত্যগত নিয়ম হলো: একটি যৌক্তিক রিলিজের সব ইনস্ট্যান্সে একই সামঞ্জস্যপূর্ণ বিল্ড আর্টিফ্যাক্ট ও একই ডিপ্লয়মেন্ট পরিচয় থাকা উচিত।

generateBuildId Next.js বিল্ডকে চিহ্নিত করে। deploymentId বিশেষভাবে সংস্করণ-অসামঞ্জস্য সুরক্ষা ও ক্যাশ-বাস্টিংয়ের জন্য নথিভুক্ত। দুটিকে সমার্থক ভাবলে ডিবাগ করা কঠিন হয়।

নতুন ডকুমেন্টের আগে তার অ্যাসেট প্রকাশ করব

নিরাপদ ডিপ্লয়মেন্টের ক্রম ইচ্ছাকৃতভাবে অসম। নতুন অপরিবর্তনীয় অ্যাসেট কেউ উল্লেখ করার আগেই থাকতে পারে। কিন্তু নতুন HTML এমন অ্যাসেট উল্লেখ করা শুরু করবে না, যা এখনও পাওয়া যাচ্ছে না।

ধারণাগতভাবে আমি এই ক্রম চাই:

1. রিলিজ B একবার বিল্ড করুন
2. B-এর /_next/static অ্যাসেট আপলোড করুন
3. প্রয়োজনীয় অ্যাসেট সত্যিই পাওয়া যাচ্ছে কি না যাচাই করুন
4. B-এর সার্ভার বা রানটাইম প্রস্তুত করুন
5. স্বাস্থ্য পরীক্ষা চালান
6. নতুন ডকুমেন্ট ট্র্যাফিক এক ধাপে B-তে সরান
7. A-এর স্থির অ্যাসেট উপলভ্য রাখুন
8. B মনিটর করুন
9. পুরোনো অ্যাসেট পরে পরিষ্কার করুন

স্থির এক্সপোর্ট-এর ক্ষেত্রেও একই নীতি: আগে সংস্করণযুক্ত অ্যাসেট, পরে সেগুলো উল্লেখ করা HTML। SSR যদি রিভার্স প্রক্সির পেছনে থাকে, নতুন সার্ভার প্রস্তুত ও যাচাই করে তারপর ট্র্যাফিক বদলান।

রোলব্যাকও একইভাবে কাজ করা উচিত। আগের রিলিজের ডিরেক্টরি ও অ্যাসেট রেখে দিলে চাপের মধ্যে পুরোনো ফাইল পুনর্গঠন না করেই ফিরে যাওয়া যায়।

তবে এতে প্রতিটি রোলব্যাক নিরাপদ হয় না। অসামঞ্জস্যপূর্ণ ডেটাবেস মাইগ্রেশন বা API চুক্তির পরিবর্তন পুরোনো অ্যাপ্লিকেশনকে ভেঙে দিতে পারে, JavaScript ফাইলগুলো থাকলেও। অ্যাসেট রিটেনশন শুধু স্থির সামঞ্জস্যের সমস্যাটি সমাধান করে।

সহজ নিজস্ব হোস্টিং-এ শেয়ার করা অপরিবর্তনীয় অ্যাসেট ডিরেক্টরি ভালো কাজ করে

ছোট Nginx-ভিত্তিক ডিপ্লয়মেন্টে সক্রিয় অ্যাপ্লিকেশন রিলিজকে শেয়ার করা স্থির-অ্যাসেট স্টোর থেকে আলাদা রাখা সহজ ও কার্যকর।

উদাহরণস্বরূপ কাঠামো:

/srv/app/releases/2026-08-13-a/
/srv/app/releases/2026-08-13-b/
/srv/app/current -> /srv/app/releases/2026-08-13-b/

/srv/app/shared/_next/static/...

প্রতিটি ডিপ্লয়মেন্ট নতুন /_next/static ফাইল শেয়ার করা ডিরেক্টরিতে যোগ করবে, কিন্তু রিটেনশন উইন্ডোর মধ্যে থাকা রিলিজের পুরোনো ফাইল মুছবে না। Nginx পথটি অপরিবর্তনীয় ক্যাশ নীতিতে সার্ভ করতে পারে:

location ^~ /_next/static/ {
    root /srv/app/shared;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

এটি শুধু উদাহরণ, আমার ঠিক ব্যবহৃত Nginx কনফিগারেশন নয়। বাস্তব ডিপ্লয়মেন্টে অনুমতি, MIME ধরন, কমপ্রেশন ভ্যারিয়েন্ট, CDN-এর আচরণ এবং সঠিক আউটপুট বিন্যাস বিবেচনা করতে হবে।

মূল স্থাপত্যগত বিষয়টি লাইফসাইকেল: বর্তমান রিলিজের দিকে নির্দেশ করা পরিবর্তনশীল পয়েন্টার আর হ্যাশযুক্ত অ্যাসেটের মূলত শুধু-যোগ হওয়া স্টোরের জীবনচক্র এক নয়।

HTML-এর ক্যাশ নীতি হ্যাশযুক্ত চাঙ্কের নীতি থেকে আলাদা হওয়া দরকার

এই সমস্যাটি নিজে তৈরি করার সবচেয়ে সহজ উপায় হলো HTML-কে এমনভাবে ক্যাশ করা, যেন সেটিও নামের মাধ্যমে সংস্করণযুক্ত অপরিবর্তনীয় অ্যাসেট।

ডায়নামিকভাবে রেন্ডার করা পেজে Next.js সাধারণত ব্যবহারকারী-নির্দিষ্ট আউটপুটকে পুনর্ব্যবহার না করার ক্যাশ নীতি দেয়। স্থির ও ISR পেজের নীতি আলাদা, এবং CDN সেগুলো বৈধভাবেই ক্যাশ করতে পারে। Nginx দিয়ে সার্ভ করা স্থির এক্সপোর্ট-এ আচরণ আরও বেশি নির্ভর করে অপারেটর কোন হেডার দিচ্ছেন তার ওপর।

তাই পুরো “ওয়েবসাইট”-এর জন্য একটাই ক্যাশ নিয়ম ব্যবহার করি না। আমি অবজেক্টের ধরন আলাদা করি:

হ্যাশযুক্ত /_next/static অ্যাসেট
    দীর্ঘ max-age
    immutable
    রেখে দেওয়া নিরাপদ

HTML / রুট ডকুমেন্ট
    নতুন রিলিজে যেতে পারা দরকার
    নীতি রেন্ডারিং মডেলের ওপর নির্ভর
    যে অ্যাসেট উল্লেখ করে তার চেয়ে বেশি পুরোনো থাকা উচিত নয়

RSC / নেভিগেশন / API ডেটা
    সামঞ্জস্য ও সতেজতার আলাদা নিয়ম

CDN থাকলে ক্যাশের নকশা অনুযায়ী ডিপ্লয়মেন্টের পরে নতুন ডকুমেন্টের রুট ক্যাশ থেকে সরাতে হতে পারে। কিন্তু শুধু নতুন রিলিজ এসেছে বলে পুরোনো হ্যাশযুক্ত চাঙ্ক ক্যাশ থেকে সরিয়ে দেওয়া অনেক সময় উল্টো ক্ষতি করে: অরিজিনও যদি ফাইলগুলো মুছে ফেলে, তাহলে পুরোনো ট্যাবকে বাঁচাতে পারত এমন শেষ কপিটিও হারিয়ে যায়।

Next.js-এর CDN ক্যাশিং নির্দেশিকা এখানে কাজে লাগে, কারণ এটি পেজ ক্যাশিংকে দীর্ঘমেয়াদি অপরিবর্তনীয় অ্যাসেট নীতি থেকে আলাদা করে।

স্বয়ংক্রিয় রিলোড রিকভারির উপায়, মূল ডিপ্লয়মেন্ট কৌশল নয়

চাঙ্ক ব্যর্থ হলে প্রচলিত উত্তর হলো “পেজ রিলোড করুন।” এটি অনেক সময় কাজ করে, কারণ সম্পূর্ণ নেভিগেশন বর্তমান ডকুমেন্ট আনে, আর সেটি বর্তমান বিল্ডের অ্যাসেট উল্লেখ করে।

কিন্তু প্রতিটি স্ক্রিপ্ট ত্রুটিতে রিলোড করলে নতুন সমস্যা তৈরি হয়:

  • তৃতীয় পক্ষের স্ক্রিপ্ট ব্যর্থতা অপ্রয়োজনীয় রিলোড ঘটাতে পারে;
  • সত্যিকারের সার্ভার বিভ্রাট অসীম রিলোড লুপ তৈরি করতে পারে;
  • সংরক্ষণ না করা ফর্মের তথ্য হারাতে পারে;
  • সম্পূর্ণ নেভিগেশনে React কম্পোনেন্টের স্থানীয় স্টেট হারায়;
  • বর্তমান রিলিজটাই ভাঙা হলে রিলোডের পর একই ব্যর্থতা আবার হবে।

বর্তমান Next.js ডকুমেন্টেশনও সতর্ক করে যে সংস্করণ-অসামঞ্জস্য সামলাতে পূর্ণ নেভিগেশন করলে useState-এর মতো কম্পোনেন্ট স্টেট হারাতে পারে, যদিও URL বা স্থায়ী ব্রাউজার স্টোরেজে রাখা স্টেট টিকে থাকতে পারে।

ক্লায়েন্ট-সাইড রিকভারি যোগ করলে আমি সেটিকে খুব নির্দিষ্ট এবং একবারের জন্য রাখব। উদাহরণ:

const RECOVERY_KEY = 'next-chunk-recovery-attempted'

function isOwnNextAsset(url: string) {
  try {
    const parsed = new URL(url, window.location.href)
    return (
      parsed.origin === window.location.origin &&
      parsed.pathname.startsWith('/_next/static/')
    )
  } catch {
    return false
  }
}

window.addEventListener(
  'error',
  (event) => {
    const target = event.target
    if (!(target instanceof HTMLScriptElement)) return
    if (!isOwnNextAsset(target.src)) return

    reportChunkFailure({
      page: window.location.href,
      asset: target.src,
    })

    if (sessionStorage.getItem(RECOVERY_KEY)) return

    sessionStorage.setItem(RECOVERY_KEY, '1')
    window.location.reload()
  },
  true,
)

এটি ইচ্ছাকৃতভাবে শুধু উদাহরণ। প্রোডাকশন কোডে CSS চাঙ্ক, পরিচিত ফ্রেমওয়ার্ক ত্রুটির ধরন, রিলোডে কাজ হারাতে পারে এমন ওয়ার্কফ্লো এবং সুস্থ লোডের পরে রিকভারি মার্কার পরিষ্কার করার নিয়মও বিবেচনা করতে হবে।

এডিটর, চেকআউট বা দীর্ঘ ফর্মে জোর করে রিফ্রেশ করার বদলে “নতুন সংস্করণ এসেছে; কাজ সংরক্ষণ করে রিলোড করুন” ব্যানার দেখানো ভালো হতে পারে।

মনিটরিং ডেটা থেকে বোঝা উচিত এটি সত্যিই সংস্করণ-অসামঞ্জস্য কি না

শুধু “স্ক্রিপ্ট লোড হয়নি” যথেষ্ট নয়। মুছে ফেলা পুরোনো চাঙ্ক আর এলোমেলো নেটওয়ার্ক ব্যর্থতা আলাদা করতে ডিপ্লয়মেন্ট-সংক্রান্ত প্রসঙ্গ দরকার।

কাজের ক্ষেত্রগুলো হলো:

  • ব্যর্থ অ্যাসেটের URL;
  • বর্তমান পেজের URL;
  • রিসোর্সটি নিজের নাকি তৃতীয় পক্ষের;
  • ক্লায়েন্টের কাছে দৃশ্যমান রিলিজ বা ডিপ্লয়মেন্ট আইডি;
  • ব্রাউজার ও অপারেটিং সিস্টেম;
  • navigator.onLine—শুধু দুর্বল সংকেত, সংযোগের প্রমাণ নয়;
  • পেজ লোড হওয়ার পর কত সময় গেছে;
  • ত্রুটিটি ডিপ্লয়মেন্টের কত কাছাকাছি ঘটেছে;
  • রিকভারি আগে চেষ্টা করা হয়েছে কি না;
  • সার্ভার-সাইডে দেখা গেলে HTTP স্ট্যাটাস;
  • অরিজিন বা প্রক্সিতে কোন রিলিজ আসলে রিকোয়েস্টটি সার্ভ করেছে।

এরপর প্যাটার্ন অনেক বেশি অর্থপূর্ণ হয়।

বিভিন্ন নেটওয়ার্কের বহু ব্যবহারকারী যদি রিলিজের ঠিক পরে পুরোনো হ্যাশযুক্ত URL চান এবং অরিজিন 404 ফেরায়, খুব তাড়াতাড়ি অ্যাসেট মুছে ফেলা শক্তিশালী ব্যাখ্যা হয়ে ওঠে। একজন ব্যবহারকারী যদি HTTP রেসপন্স ছাড়াই নেটওয়ার্ক-স্তরের ব্যর্থতা দেখেন, সংস্করণ-অসামঞ্জস্যের প্রমাণ অনেক দুর্বল। আর চাঙ্ক 200 ফেরালেও যদি বডি HTML হয় বা MIME ধরন ভুল হয়, সমস্যা রিটেনশন নয়—রাউটিং বা প্রক্সিতে।

নিজস্ব চাঙ্ক ব্যর্থতার সতর্কতাও তৃতীয় পক্ষের রিসোর্স ব্যর্থতা থেকে আলাদা রাখব। আমার মূল লগ থেকে সরাসরি সমর্থন পাওয়া পরিবর্তন এটিই: গুরুত্বপূর্ণ সংকেত অনেক অপ্রাসঙ্গিক ব্রাউজার শব্দের সঙ্গে মিশে ছিল।

পুনরুৎপাদনের পরীক্ষা সহজ, তবে পুরোনো ট্যাবটিকে পুরোনোই রাখতে হবে

এই ধরনের সমস্যা সাধারণ রিলিজ পরীক্ষায় সহজে ধরা পড়ে না, কারণ ডিপ্লয়মেন্টের পর ডেভেলপাররা প্রায়ই সঙ্গে সঙ্গে রিফ্রেশ করেন। এতে ঠিক যে অবস্থাটি পরীক্ষা করা দরকার, সেটিই নষ্ট হয়ে যায়।

আরও ভালো একটি হাতে-কলমে পরীক্ষা:

  1. রিলিজ A ডিপ্লয় করুন;
  2. বাস্তবসম্মত ব্রাউজার ক্যাশসহ প্রোডাকশনের মতো একটি ট্যাব খুলুন;
  3. অ্যাপ্লিকেশনের শুধু কিছু অংশ ব্যবহার করুন, যাতে কিছু রুট বা বিলম্বে লোড হওয়া ফিচার এখনও লোড না হয়;
  4. ট্যাবটি খোলা রাখুন;
  5. রিলিজ B ডিপ্লয় করুন;
  6. পুরোনো ট্যাব রিফ্রেশ করবেন না;
  7. এমন রুট বা ডায়নামিক ফিচার খুলুন যার কোড আগে ডাউনলোড হয়নি;
  8. Network এবং Console দেখুন;
  9. পুরোনো অ্যাসেট URL এখনও 200 ফেরায় কি না যাচাই করুন;
  10. অসামঞ্জস্য ধরা পড়লে নিয়ন্ত্রিত পূর্ণ নেভিগেশন হচ্ছে কি না যাচাই করুন।

একই পরীক্ষা CDN সামনে রেখে, ধাপে ধাপে ডিপ্লয়মেন্টের সময় একাধিক ইনস্ট্যান্সে এবং নির্ধারিত সংরক্ষণকাল শেষ হওয়ার পরও করব।

একটি সূক্ষ্ম ভুল হলো পুরো পরীক্ষায় DevTools-এর “Disable cache” চালু রাখা। কিছু রোগ নির্ণয়ে এটি কাজে লাগে, কিন্তু আমরা যে ব্রাউজার আচরণটি পরীক্ষা করছি সেটিই বদলে দেয়। দীর্ঘসময় খোলা ট্যাবের ঘটনা বাস্তবসম্মত ক্যাশ দিয়েও পরীক্ষা করা উচিত।

পুরোনো ফাইল রেখে দিলেই প্রতিটি চাঙ্ক ব্যর্থতা ঠিক হয় না

রিটেনশন উপকারী কারণ এটি একটি নির্দিষ্ট ব্যর্থতার প্রক্রিয়া সমাধান করে। এটিকে নতুন সর্বজনীন ব্যাখ্যা বানানো উচিত নয়।

নিজস্ব চাঙ্ক ব্যর্থ হতে পারে কারণ:

  • রিকোয়েস্ট সার্ভার পর্যন্ত পৌঁছায়নি;
  • সংযোগ মাঝপথে কেটে গেছে;
  • ব্রাউজার এক্সটেনশন সেটি ব্লক করেছে;
  • CDN প্রান্তিক সার্ভার সাময়িকভাবে ব্যর্থ হয়েছে;
  • Nginx পথটি ভুলভাবে রুট করেছে;
  • সার্ভার JavaScript-এর বদলে HTML ত্রুটি-পেজ ফিরিয়েছে;
  • কমপ্রেশন বা Content-Encoding নষ্ট হয়েছে;
  • ফাইলের অনুমতি ভুল ছিল;
  • আংশিক ডিপ্লয়মেন্ট চাঙ্কটি আপলোডই করেনি;
  • ফাইল ছিল, কিন্তু খুব তাড়াতাড়ি মুছে ফেলা হয়েছে;
  • ক্লায়েন্ট ও সার্ভার অসামঞ্জস্যপূর্ণ ডিপ্লয়মেন্টে ছিল।

রেসপন্স কোড ও সময় গুরুত্বপূর্ণ। প্রতিটি রিলিজের পরে পুরোনো হ্যাশযুক্ত URL-এ বারবার 404 পাওয়া আর একটি মোবাইল নেটওয়ার্কে ERR_CONNECTION_RESET পাওয়া একেবারে আলাদা গল্প বলে।

এই কারণেই আমি মূল ঘটনাটিকে “আমি প্রমাণ করেছি পুরোনো HTML সাইট ভেঙেছে” বলে লিখব না। আমি তা প্রমাণ করিনি। আমি একটি সত্যিকারের নিজস্ব চাঙ্ক ব্যর্থতা দেখেছি, এবং সেখান থেকে সংস্করণ-অসামঞ্জস্যকে এমন একটি গুরুতর ব্যর্থতার ধরন হিসেবে চিহ্নিত করেছি যার বিরুদ্ধে নকশাতেই ব্যবস্থা নেওয়া উচিত।

নিরাপদ ডিপ্লয়মেন্ট পুরোনো ক্লায়েন্টকেও রিলিজের অংশ ধরে

গভীর ভুলটি হলো ডিপ্লয়মেন্টকে এমন একটি মুহূর্ত ভাবা, যখন A হঠাৎ করে B হয়ে যায়।

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

তাই বাস্তব রিলিজ-পৃষ্ঠ একটি বিন্দু নয়, সময়ের একটি পরিসর।

আমার Next.js ডিপ্লয়মেন্টের নিয়মগুলো এখন এই ধারণার ওপর দাঁড়ানো:

  • প্রতি যৌক্তিক রিলিজ একবার বিল্ড করুন। রেপ্লিকাগুলো যেন চুপচাপ আলাদা আউটপুট তৈরি না করে।
  • যে ডকুমেন্ট অ্যাসেট উল্লেখ করবে, তার আগে অপরিবর্তনীয় অ্যাসেট প্রকাশ করুন।
  • পুরোনো হ্যাশযুক্ত অ্যাসেট পরিকল্পিত সামঞ্জস্য উইন্ডো পর্যন্ত রাখুন।
  • পরিবর্তনশীল HTML-কে হ্যাশযুক্ত চাঙ্কের মতো ক্যাশ নীতি দেবেন না।
  • যেখানে ডিপ্লয়মেন্ট মডেলে সংস্করণ-অসামঞ্জস্য হতে পারে, সেখানে deploymentId ব্যবহার করুন।
  • প্ল্যাটফর্ম সত্যিই সংস্করণ-সচেতন রাউটিং দিলে তবেই তার সংস্করণ-অসামঞ্জস্য সুরক্ষার ওপর নির্ভর করুন।
  • রিকভারি একবারের এবং ব্যবহারকারীর স্টেট সম্পর্কে সচেতন রাখুন।
  • নিজস্ব চাঙ্ক ব্যর্থতাকে আলাদা প্রোডাকশন সংকেত হিসেবে মনিটর করুন।
  • পুরোনো খোলা ট্যাব রেখে ডিপ্লয়মেন্ট পরীক্ষা করুন।
  • পুরোনো অ্যাসেট পরে পরিষ্কার করুন, নতুন রিলিজ চালু করার সময় নয়।

আমি এখন যে নিয়মটি মেনে চলি

সবুজ বিল্ড এবং সদ্য খোলা সুস্থ পেজ প্রমাণ করে না যে ডিপ্লয়মেন্ট আগে থেকেই অ্যাপ্লিকেশনে থাকা ব্যবহারকারীর জন্য নিরাপদ।

পুরোনো ট্যাব কোনো অবশিষ্ট আবর্জনা নয়। এটি সত্যিকারের ক্লায়েন্ট, যা সত্যিকারের আগের রিলিজ চালাচ্ছে।

এইভাবে ভাবা শুরু করার পর চাঙ্কের সমস্যা অনেক কম রহস্যময় হয়েছে। কনটেন্ট হ্যাশ অ্যাসেটকে স্থিতিশীল পরিচয় দেয়। দীর্ঘমেয়াদি ক্যাশ সেই পরিচয়কে কার্যকর করে। কিন্তু ডিপ্লয়মেন্টকে যথেষ্ট সময় সেই পরিচয় সম্মান করতে হবে, অথবা ক্লায়েন্টকে বর্তমান সংস্করণে যাওয়ার নিয়ন্ত্রিত পথ দিতে হবে।

প্রতিটি পুরোনো রিলিজ চিরকাল চালু রাখার দরকার নেই। দরকার এমন একটি ব্যবস্থা, যা সেই সময়কাল সামলাতে পারে যখন পুরোনো ক্লায়েন্ট ও নতুন সার্ভার বৈধভাবেই পাশাপাশি থাকে।

এখন আমার কাছে ডিপ্লয়মেন্টের চুক্তিটি হলো: নতুন ব্যবহারকারী নতুন রিলিজ পাবে, পুরোনো ব্যবহারকারী তার চলমান সংস্করণ এখনও যে ফাইল চাইতে জানে তা হারাবে না, আর বাকি যেকোনো অসামঞ্জস্য ভাঙা পেজে নয়—ইচ্ছাকৃত রিকভারি পথে শেষ হবে।