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

একটি একক ধীর অনুরোধ সাধারণত "শুধু ধীর" থাকে না। এটি অপেক্ষা করার সময় একটি goroutine জীবিত রাখে, বাফার ও রেসপন্স অবজেক্টের জন্য মেমরি ধরে রাখে, এবং প্রায়ই একটি ডাটাবেস সংযোগ বা পুলের স্লট দখল করে। যখন প্রচুর ধীর অনুরোধ একত্রে জমে যায়, আপনার API কার্যকর কাজ করা বন্ধ করে দেয় কারণ সীমিত রিসোর্সগুলো অপেক্ষা করে আটকে থাকে।
এটি সাধারণত তিনটি জায়গায় অনুভূত হয়। Goroutine জমে যায় এবং শিডিউলিং ওভারহেড বাড়ে, ফলে সবার জন্য লেটেনসি খারাপ হয়। ডাটাবেস পুলে ফ্রি সংযোগ শেষ হয়ে যায়, তাই দ্রুত কুয়েরিও ধীরগুলোর পিছনে কিউ-তে দাঁড়াতে শুরু করে। ইন-ফ্লাইট ডেটা ও আংশিকভাবে তৈরি হওয়া রেসপন্স থেকে মেমরি বাড়ে, যা GC কাজ বাড়ায়।
আরও সার্ভার যোগ করলেই সাধারণত সমস্যা মিটে না। যদি প্রতিটি ইনস্ট্যান্স একই বটলনেকে (একটি ছোট DB পুল, একটি ধীর আপস্ট্রিম, শেয়ার্ড রেট লিমিট) আঘাত করে, আপনি কেবল কিউটাকে অন্যত্র স্থানান্তর করছেন এবং বেশি খরচ করছেন যখন এররগুলো এখনও বাড়ছে।
একটি হ্যান্ডলারের কল্পনা করুন যা ফ্যান আউট করে: এটি PostgreSQL থেকে ইউজার লোড করে, একটি পেমেন্ট সার্ভিসকে কল করে, তারপর একটি রিকমেন্ডেশন সার্ভিসকে কল করে। যদি রিকমেন্ডেশন কল হ্যাং করে এবং কিছুই তা বাতিল না করে, অনুরোধ কখনও শেষ হবে না। DB সংযোগটি ফিরে পেতে পারে, কিন্তু goroutine এবং HTTP ক্লায়েন্ট রিসোর্সগুলো আটকে থাকে। সেইটা কয়েকশো অনুরোধে গুণ করলে আপনি একটি ধীর মেল্টডাউন পাবেন।
লক্ষ্য সরল: একটি পরিষ্কার সময়সীমা নির্ধারণ করুন, সময় শেষ হলে কাজ বন্ধ করুন, রিসোর্স মুক্ত করুন, এবং একটি পূর্বানুমেয় ত্রুটি ফেরত দিন। Go কনটেক্সট টাইমআউট প্রতিটি ধাপে একটি ডেডলাইন দেয় যাতে কাজটি তখনই থেমে যায় যখন ব্যবহারকারী আর অপেক্ষা করছে না।
A context.Context একটি ছোট অবজেক্ট যা আপনি কল চেইন দিয়ে পাস করেন যাতে প্রতিটি লেয়ার একটি বিষয় নিয়ে একমত হয়: এই অনুরোধটি কখন বন্ধ হওয়া উচিত। টাইমআউট হলো সাধারণ উপায় যাতে একটি ধীর নির্ভরশীলতা আপনার সার্ভারকে বাঁধা না দেয়।
একটি কনটেক্সট তিন ধরনের তথ্য বহন করতে পারে: একটি ডেডলাইন (কখন কাজ বন্ধ করতে হবে), একটি বাতিল সিগন্যাল (কেউ আগে থেকে বন্ধ করার সিদ্ধান্ত নিয়েছে), এবং কয়েকটি রিকোয়েস্ট-স্কোপড ভ্যালু (এগুলো সংযমের সাথে ব্যবহার করুন, এবং বড় ডেটার জন্য কখনই ব্যবহার করবেন না)।
বাতিলকরণ জাদু নয়। একটি কনটেক্সট Done() নামক একটি চ্যানেল উন্মুক্ত করে। যখন এটি বন্ধ হয়, অনুরোধ বাতিল হয়েছে বা সময়সীমা পূর্ণ হয়েছে। কনটেক্সটকে সম্মান করা কোড Done() চেক করে (প্রায়ই একটি select দিয়ে) এবং আগে ফিরে আসে। আপনি ctx.Err() চেক করে জানতে পারেন কেন এটি শেষ হয়েছে, সাধারণত context.Canceled বা context.DeadlineExceeded।
context.WithTimeout ব্যবহার করুন "X সেকেন্ড পর বন্ধ করুন" এর জন্য। context.WithDeadline ব্যবহার করুন যখন আপনি ইতিমধ্যেই সুনির্দিষ্ট কাটঅফ সময় জানেন। context.WithCancel ব্যবহার করুন যখন একটি পেরেন্ট কন্ডিশন কাজ বন্ধ করা উচিত (ক্লায়েন্ট বিচ্ছিন্ন হয়েছে, ব্যবহারকারী চলে গেছে, আপনার কাছে ইতিপূর্বে উত্তর আছে)।
যখন একটি কনটেক্সট বাতিল হয়ে যায়, সঠিক আচরণ সাধারণত কাছে নয় কিন্তু গুরুত্বপূর্ণ: কাজ থামান, ধীর I/O-তে অপেক্ষা বন্ধ করুন, এবং একটি পরিষ্কার ত্রুটি ফেরত দিন। যদি একটি হ্যান্ডলার ডাটাবেস কুয়েরির জন্য অপেক্ষা করছে এবং কনটেক্সট শেষ হয়ে যায়, দ্রুত ফিরে আসুন এবং ডাটাবেস কলটি বাতিল হোক যদি এটি কনটেক্সট সমর্থন করে।
ধীর অনুরোধ বন্ধ করার সবচেয়ে নিরাপদ জায়গা হল সেই সীমানা যেখানে ট্রাফিক আপনার সার্ভিসে ঢুকছে। যদি একটি অনুরোধ টাইমআউট হবে, আপনি চান সেটি পূর্বানুমেয় এবং শুরুতেই ঘটে, না এমনভাবে যে এটি goroutine, DB সংযোগ এবং মেমরি আটকে রাখে।
এজে (লোড ব্যালান্সার, API গেটওয়ে, রিভার্স প্রোক্সি) শুরু করুন এবং প্রতিটি অনুরোধ কতক্ষণ জীবিত থাকতে পারবে তার একটি কঠোর ক্যাপ সেট করুন। এটি আপনার Go সার্ভিসকে রক্ষা করবে এমনকি যদি কোনো হ্যান্ডলার টাইমআউট সেট করতে ভুলে যায়।
আপনার Go সার্ভারের ভিতরে, HTTP টাইমআউট সেট করুন যাতে সার্ভার ধীর ক্লায়েন্ট বা স্টল করা রেসপন্সের জন্য অনির্দিষ্টকাল অপেক্ষা না করে। কমপক্ষে হেডার পড়া, পুরো রিকোয়েস্ট বডি পড়া, রেসপন্স লেখা, এবং আইডল সংযোগ রাখা—এসবের জন্য টাইমআউট কনফিগার করুন।
আপনার প্রোডাক্টের সাথে মিল রেখে একটি ডিফল্ট অনুরোধ বাজেট নির্বাচন করুন। অনেক API-র জন্য 1 থেকে 3 সেকেন্ড একটি বাস্তবসম্মত শুরু বিন্দু, এবং ধীর অপারেশনের জন্য (যেমন এক্সপোর্ট) একটি উচ্চতর সীমা রাখুন। সঠিক সংখ্যা থেকে বেশি গুরুত্বপূর্ণ হলো ধারাবাহিকতা, পরিমাপ, এবং আছে হলেও ব্যতিক্রমের জন্য স্পষ্ট নিয়ম থাকা।
স্ট্রিমিং রেসপন্সগুলো একটু আলাদা যত্ন দাবি করে। ভুলবশত একটি অসীম স্ট্রিম তৈরি করা সহজ, যেখানে সার্ভার সংযোগ খোলা রেখেই ছোট ছোট টুকরা লিখতে থাকে, অথবা প্রথম বাইটের আগে অনির্দিষ্টকাল অপেক্ষা করে। আগেই সিদ্ধান্ত নিন একটি এন্ডপয়েন্ট কি সত্যিই স্ট্রিম কি না। না হলে, মোট সময়ের সর্বোচ্চ সীমা এবং প্রথম বাইট পর্যন্ত সর্বোচ্চ সময় বলবৎ করুন।
একবার সীমানায় একটি স্পষ্ট ডেডলাইন থাকলে, পুরো অনুরোধ জুড়ে সেই ডেডলাইন প্রোপাগেট করা অনেক সহজ হয়।
শুরু করার সবচেয়ে সরল জায়গা হলো HTTP হ্যান্ডলার। একটি অনুরোধ এখানেই আপনার সিস্টেমে প্রবেশ করে, তাই এটি কঠোর সীমা রাখার প্রাকৃতিক জায়গা।
একটি নতুন কনটেক্সট ডেডলাইন সহ তৈরি করুন, এবং নিশ্চিত করে নিন আপনি সেটি ক্যান্সেল করছেন। তারপর সেই কনটেক্সট এমন যেকোনো জায়গায় পাঠান যা ব্লক করতে পারে: ডাটাবেস কাজ, HTTP কল, বা ধীর হিসাব।
func (s *Server) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
userID := r.URL.Query().Get("id")
if userID == "" {
http.Error(w, "missing id", http.StatusBadRequest)
return
}
user, err := s.loadUser(ctx, userID)
if err != nil {
writeError(w, ctx, err)
return
}
writeJSON(w, http.StatusOK, user)
}
একটি ভালো নীতিঃ যদি একটি ফাংশন I/O-এ অপেক্ষা করতে পারে, এটি context.Context গ্রহণ করা উচিত। হ্যান্ডলারগুলো পড়ার মত রাখুন ছোট হেল্পার ফাংশনের মাধ্যমে যেমন loadUser।
func (s *Server) loadUser(ctx context.Context, id string) (User, error) {
return s.repo.GetUser(ctx, id) // repo should use QueryRowContext/ExecContext
}
যদি ডেডলাইন পেরিয়ে যায় (বা ক্লায়েন্ট বিচ্ছিন্ন হয়), কাজ বন্ধ করুন এবং ব্যবহারকারীর-বান্ধব রেসপন্স দিন। একটি সাধারণ ম্যাপিং হলো context.DeadlineExceeded → 504 Gateway Timeout, এবং context.Canceled → "client is gone" (প্রায়ই কোনো বডি ছাড়া)।
func writeError(w http.ResponseWriter, ctx context.Context, err error) {
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
if errors.Is(err, context.Canceled) {
// Client went away. Avoid doing more work.
return
}
http.Error(w, "internal error", http.StatusInternalServerError)
}
এই প্যাটার্ন পাইল-আপ প্রতিরোধ করে। একবার টাইমার শেষ হলে, চেইনের প্রতিটি কনটেক্সট-অ্যাওয়ার ফাংশন একই স্টপ সিগন্যাল পায় এবং দ্রুত এক্সিট করতে পারে।
একবার আপনার হ্যান্ডলারে ডেডলাইন সহ একটি কনটেক্সট থাকলে, সবচেয়ে গুরুত্বপূর্ণ নিয়ম হলো: একই ctx ডাটাবেস কল পর্যন্ত পাঠান। এভাবে টাইমআউট কাজকে থামে দেয় কেবল হ্যান্ডলার অপেক্ষা করা বন্ধ না করে।
database/sql ব্যবহার করলে কনটেক্সট-সক্ষম মেথডগুলোকে পছন্দ করুন:
func (s *Server) getUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
row := s.db.QueryRowContext(ctx,
"SELECT id, email FROM users WHERE id = $1",
r.URL.Query().Get("id"),
)
var id int64
var email string
if err := row.Scan(&id, &email); err != nil {
// handle below
}
}
যদি হ্যান্ডলার বাজেট 2 সেকেন্ড হয়, ডাটাবেসকে তার কেবল একটি অংশ দিন। JSON এনকোডিং, অন্যান্য নির্ভরশীলতা, এবং এরর হ্যান্ডলিংয়ের জন্য কিছু সময় রাখুন। একটি সহজ শুরু হলো মোট বাজেটের 30% থেকে 60% ডাটাবেসকে দেওয়া। 2 সেকেন্ড বাজেটে সেটা হতে পারে 800ms থেকে 1.2s।
যখন কনটেক্সট বাতিল হয়, ড্রাইভার Postgres-কে কুয়েরি থামাতে বলে। সাধারণত সংযোগ পুলে ফিরে আসে এবং পুনরায় ব্যবহার করা যায়। যদি বাতিলকরণ নেটওয়ার্ক সমস্যার সময় ঘটে, ড্রাইভার সেই সংযোগ ফেলে দিয়ে পরে নতুন একটি খুলতে পারে। যেভাবেই হোক, আপনি একটি goroutine অনন্তকাল অপেক্ষা করলে তা আটকাতে পারবেন।
এরর চেক করার সময় টাইমআউটকে বাস্তব DB ব্যর্থতার থেকে আলাদা করুন। যদি errors.Is(err, context.DeadlineExceeded) হয়, আপনি সময় শেষ করেছেন এবং একটি টাইমআউট ফেরত করা উচিত। যদি errors.Is(err, context.Canceled) হয়, ক্লায়েন্ট চলে গিয়েছিল এবং আপনাকে চুপচাপ থেমে যেতে হবে। অন্যান্য ত্রুটিগুলো হলো স্বাভাবিক কুয়েরি সমস্যাগুলি (ভুল SQL, মিসিং রো, পারমিশন)।
আপনার হ্যান্ডলারের একটি ডেডলাইন থাকলে, আপনার আউটবাউন্ড HTTP কলগুলোকে সেটি সম্মান করা উচিত। নাহলে ক্লায়েন্ট ছাড়বে, কিন্তু আপনার সার্ভার ধীর আপস্ট্রিমে অপেক্ষা করে goroutine, সকেট, এবং মেমরি আটকে রাখবে।
প্যারেন্ট কনটেক্সট নিয়ে আউটবাউন্ড রিকোয়েস্ট তৈরি করুন যাতে ক্যান্সেলেশন স্বয়ংক্রিয়ভাবে চলে:
func fetchUser(ctx context.Context, url string) ([]byte, error) {
// Add a small per-call cap, but never exceed the parent deadline.
ctx, cancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close() // always close, even on non-200
return io.ReadAll(resp.Body)
}
প্রতি কলের এই ছোট টাইমআউটটি একটি সেফটি নেট। প্যারেন্ট অনুরোধ ডেডলাইনই আসল বস। একটি অনুরোধের জন্য এক ঘড়ি, এবং ঝুঁকিপূর্ণ ধাপের জন্য ছোট ক্যাপ।
ট্রান্সপোর্ট লেভেলে টাইমআউটও সেট করুন। কনটেক্সট অনুরোধ বাতিল করে, কিন্তু ট্রান্সপোর্ট টাইমআউট ধীর হ্যান্ডশেক ও হেডার না পাঠানোর সার্ভারের বিরুদ্ধে সুরক্ষা দেয়।
একটি সূক্ষ্ম বিষয়ে দলেরা মাঝেমাঝে সমস্যায় পড়ে: প্রতিটি পথেই রেসপন্স বডি বন্ধ করতে হবে। যদি আপনি আগে ফিরে যান (স্ট্যাটাস কোড চেক, JSON ডিকোড ত্রুটি, কনটেক্সট টাইমআউট), তবুও বডি বন্ধ করুন। বডি লিক করলে সংযোগ পোল নীরবে শেষ হয়ে যেতে পারে এবং "র্যান্ডম" লেটেনসি স্পাইক তৈরি করতে পারে।
একটি বাস্তব দৃশ্য: আপনার API একটি পেমেন্ট প্রোভাইডারকে কল করে। ক্লায়েন্ট 2 সেকেন্ড পরে টাইমআউট করে, কিন্তু আপস্ট্রিম 30 সেকেন্ড হ্যাং করে। অনুরোধ বাতিলকরণ ও ট্রান্সপোর্ট টাইমআউট না থাকলে, আপনি প্রতিটি পরিত্যক্ত অনুরোধের জন্য ঐ 30 সেকেন্ড অপেক্ষার মূল্য দিতে থাকবেন।
একটি অনুরোধ সাধারণত একাধিক ধীর জিনিস স্পর্শ করে: হ্যান্ডলার কাজ, একটি ডাটাবেস কুয়েরি, এবং এক বা একাধিক বাহ্যিক API। যদি আপনি প্রতিটি ধাপে প্রশস্ত টাইমআউট দেন, মোট সময় ধীরগতিতে বাড়তে থাকে যতক্ষণ না ব্যবহারকারীরা তা অনুভব করে এবং আপনার সার্ভার পাইল-আপ করে।
বাজেটিং সবচেয়ে সরল সমাধান। পুরো অনুরোধের জন্য একটি প্যারেন্ট ডেডলাইন সেট করুন, তারপর প্রতিটি নির্ভরশীলতাকে ছোট করে ভাগ দিন। চাইল্ড ডেডলাইনগুলো প্যারেন্টের চেয়ে আগে হওয়া উচিত যাতে আপনি দ্রুত ব্যর্থ হন এবং এখনও একটা পরিষ্কার ত্রুটি ফেরত দেওয়ার সময় থাকে।
বাস্তব সার্ভিসে যা কাজ করে তার কিচ্ছু নিয়ম:
একমুখি টাইমআউট যা একে অপরের বিরুদ্ধে লড়ে যায় তা এড়ান। যদি আপনার হ্যান্ডলার কনটেক্সটে 2 সেকেন্ড ডেডলাইন থাকে এবং আপনার HTTP ক্লায়েন্টের টাইমআউট 10 সেকেন্ড থাকে, আপনি সেফ কিন্তু বিভ্রান্তিকর অবস্থায় আছেন। উল্টো হলে ক্লায়েন্ট অনির্দিষ্ট কারণে আগেই কেটে দিতে পারে।
ব্যাকগ্রাউন্ড কাজের (অডিট লগ, মেট্রিক্স, ইমেইল) জন্য রিকোয়েস্ট কনটেক্সট পুনরায় ব্যবহার করবেন না। একটি আলাদা কনটেক্সট ব্যবহার করুন যার নিজস্ব সংক্ষিপ্ত টাইমআউট থাকে যাতে ক্লায়েন্ট বাতিল করলে দরকারি ক্লিনআপও মরে না যায়।
অধিকাংশ টাইমআউট বাগ হ্যান্ডলারে নেই। এগুলো এক বা দুই লেয়ার নিচে ঘটে, যেখানে ডেডলাইন নীরবে হারিয়ে যায়। আপনি সীমানায় টাইমআউট সেট করলে কিন্তু মধ্যভাগে তা উপেক্ষা করলে, তখনও goroutine, DB কুয়েরি, বা HTTP কল চলতে থাকে যখন ক্লায়েন্ট চলে গেছে।
সবচেয়ে সাধারণ প্যাটার্নগুলো সহজঃ
context.Background() (বা TODO) দিয়ে কল করা। এতে কাজ ক্লায়েন্ট ক্যান্সেল ও হ্যান্ডলার ডেডলাইন থেকে বিচ্ছিন্ন হয়ে যায়।ctx.Done() চেক না করে স্লিপ করা, রিট্রাই করা, বা লুপ চালানো। অনুরোধ বাতিল হলেও আপনার কোড অপেক্ষা চালিয়ে যায়।context.WithTimeout দিয়ে মোড়ানো। এতে অনেক টাইমার ও বিভ্রান্ত ডেডলাইন হয়।ctx দিতে ভুলে যাওয়া (DB কুয়েরি, আউটবাউন্ড HTTP, মেসেজ পাবলিশ)। যদি নির্ভরশীলতা তা উপেক্ষা করে, হ্যান্ডলার টাইমআউট কোনো কাজ করে না।একটি ক্লাসিক ব্যর্থতা: আপনি হ্যান্ডলারে 2 সেকেন্ড টাইমআউট যোগ করেন, তারপর আপনার রিপোজিটরি ডাটাবেস কুয়েরির জন্য context.Background() ব্যবহার করে। লোডে ধীর কুয়েরি ক্লায়েন্ট চলে যাওয়ার পরও চলতে থাকে, এবং কিউ বাড়তে থাকে।
মৌলিকগুলি ঠিক করুন: ctx-কে কল স্ট্যাক জুড়ে প্রথম আর্গুমেন্ট হিসেবে পাস করুন। দীর্ঘ কাজের ভিতরে দ্রুত চেক যোগ করুন যেমন:
select {
case <-ctx.Done():
return ctx.Err()
default:
}
context.DeadlineExceeded-কে টাইমআউট রেসপন্সে (অften 504) এবং context.Canceled-কে ক্লায়েন্ট-ক্যান্সেল স্টাইল রেসপন্সে (অften 408 বা 499 আপনার কনভেনশনের উপর নির্ভর করে) ম্যাপ করুন।
টাইমআউটগুলো তখনই সহায়ক যখন আপনি সেগুলো দেখতে পান এবং নিশ্চিত করতে পারেন সিস্টেম পরিষ্কারভাবে পুনরুদ্ধার করছে। যখন কিছু ধীর হয়, অনুরোধটি থামা উচিত, রিসোর্সগুলো মুক্ত হওয়া উচিত, এবং API প্রতিক্রিয়াশীল থাকা উচিত।
প্রতি অনুরোধে একই ছোট সেট ফিল্ড লগ করুন যাতে আপনি নর্মাল রিকোয়েস্ট বনাম টাইমআউট তুলনা করতে পারেন। কনটেক্সট ডেডলাইন (যদি থাকে) এবং কি কারণে কাজ শেষ হল তা অন্তর্ভুক্ত করুন।
উপযোগী ফিল্ডগুলোর মধ্যে আছে: ডেডলাইন (বা "none"), মোট সময় প্রয়োগ, বাতিলের কারণ (টাইমআউট বনাম ক্লায়েন্ট ক্যান্সেল), একটি ছোট অপারেশন লেবেল ("db.query users", "http.call billing"), এবং একটি রিকোয়েস্ট ID।
একটি ন্যূনতম প্যাটার্ন দেখতে পারেন:
start := time.Now()
deadline, hasDeadline := ctx.Deadline()
err := doWork(ctx)
log.Printf("op=%s hasDeadline=%t deadline=%s elapsed=%s err=%v",
"getUser", hasDeadline, deadline.Format(time.RFC3339Nano), time.Since(start), err)
লগ ডিবাগ করতে সাহায্য করে। মেট্রিক্স ট্রেন্ড দেখায়।
কিছু সিগন্যাল ট্র্যাক করুন যা ভুল সেট করা টাইমআউট হলে প্রথমে বাড়ে: রুট এবং ডিপেন্ডেন্সি অনুসারে টাইমআউট কাউন্ট, ইন-ফ্লাইট অনুরোধ (লোডে তা লেভেল অফ করা উচিত), DB পুল অপেক্ষার সময়, এবং লেটেনসি পারসেন্টাইল (p95/p99) সফল বনাম টাইমআউট ভাঙন করে।
ধীরতা পূর্বানুমেয় করে তুলুন। একটি ডিবাগ-এ কেবলমাত্র একটি হ্যান্ডলারে বিলম্ব যোগ করুন, একটি ডাটাবেস কুয়েরি ইচ্ছাকৃতভাবে ধীর করুন, বা একটি টেস্ট সার্ভার দিয়ে বাহ্যিক কলকে স্লিপ করান। তারপর দুটি কাজ যাচাই করুন: আপনি টাইমআউট ত্রুটি দেখেন, এবং ক্যান্সেলেশনের পরে কাজ দ্রুত বন্ধ হয়।
একটি ছোট লোড টেস্টও সাহায্য করে। 20–50 কনকারেন্ট রিকোয়েস্ট চালান 30–60 সেকেন্ড ধরে যেখানে একটি নির্দিষ্ট ডিপেন্ডেন্সি ধীর। Goroutine কাউন্ট ও ইন-ফ্লাইট অনুরোধ বেড়ে তারপর লেভেল অফ করা উচিত। যদি তারা অব্যাহতভাবে বাড়ে, কিছু কনটেক্সট ক্যান্সেলেশন উপেক্ষা করছে।
টাইমআউট সাহায্য করে যদি এগুলো যেখানে-কোথায় অনুরোধ অপেক্ষা করতে পারে সব জায়গায় প্রয়োগ করা হয়। ডিপ্লয় করার আগে আপনার কোডবেসে এক-পাস চালান এবং নিশ্চিত করুন একই নিয়ম প্রতিটি হ্যান্ডলারে মেনে চলা হয়েছে।
context.DeadlineExceeded এবং context.Canceled এর জন্য এরর চেক করে।http.NewRequestWithContext (অথবা req = req.WithContext(ctx)) ব্যবহার করে এবং ক্লায়েন্টের ট্রান্সপোর্টে টাইমআউট আছে (dial, TLS, response header)। প্রোডাকশনে http.DefaultClient-এ নির্ভর করা এড়ান।রিলিজের আগে একটি দ্রুত "ধীর ডিপেন্ডেন্সি" ড্রিল মূল্যবান। একটি SQL কুয়েরিতে কৃত্রিম 2 সেকেন্ড বিলম্ব যোগ করুন এবং তিনটি জিনিস নিশ্চিত করুন: হ্যান্ডলার সময়ের মধ্যে ফেরত দেয়, DB কলটি সত্যিই থামছে (শুধু হ্যান্ডলার অপেক্ষা না করছে), এবং লগ স্পষ্টভাবে বলে এটা DB টাইমআউট ছিল।
ধরুন একটি এন্ডপয়েন্ট আছে GET /v1/account/summary। একটি ব্যবহারকারীর এক অ্যাকশন তিনটি জিনিস ট্রিগার করে: একটি PostgreSQL কুয়েরি (অ্যাকাউন্ট ও সাম্প্রতিক কার্যকলাপ) এবং দুইটি বাহ্যিক HTTP কল (উদাহরণ: বিলিং স্ট্যাটাস চেক ও প্রোফাইল এনরিচমেন্ট লুকআপ)।
সব অনুরোধকে একটি কড়া 2 সেকেন্ড বাজেট দিন। বাজেট না থাকলে একটি ধীর ডিপেন্ডেন্সি goroutine, DB সংযোগ, এবং মেমরি আটকে রাখতে পারে যতক্ষণ না আপনার API সর্বত্র টাইমআউট করা শুরু করে।
সহজ একটি ভাগ হতে পারে: DB কুয়েরির জন্য 800ms, বাহ্যিক কল A-র জন্য 600ms, এবং বাহ্যিক কল B-র জন্য 600ms।
একবার আপনি মোট ডেডলাইন জানেন, সেটি নিচে পাঠান। প্রতিটি ডিপেন্ডেন্সিকে তার নিজস্ব ছোট টাইমআউট দিন, কিন্তু তা এখনও প্যারেন্ট থেকে ক্যান্সেলেশন উত্তরাধিকারী হবে।
func AccountSummary(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
dbCtx, dbCancel := context.WithTimeout(ctx, 800*time.Millisecond)
defer dbCancel()
aCtx, aCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer aCancel()
bCtx, bCancel := context.WithTimeout(ctx, 600*time.Millisecond)
defer bCancel()
// Use dbCtx for QueryContext, aCtx/bCtx for outbound HTTP requests.
}
যদি বাহ্যিক কল B ধীর হয়ে 2.5 সেকেন্ড নেয়, আপনার হ্যান্ডলার 600ms-এ অপেক্ষা বন্ধ করে, ইন-ফ্লাইট কাজ বাতিল করে, এবং ক্লায়েন্টকে একটি স্পষ্ট টাইমআউট রেসপন্স দেয়। ক্লায়েন্ট একটি ধীর স্পিনার না দেখে দ্রুত ব্যর্থতা পায়।
আপনার লগে স্পষ্ট ভাবে দেখা উচিত কোনটিই বাজেট ব্যবহার করেছে—for example: DB দ্রুত শেষ করেছে, external A সফল হয়েছে, external B তার ক্যাপে পৌঁছে context deadline exceeded ফিরিয়েছে।
একটি বাস্তব এন্ডপয়েন্ট একবার টাইমআউট ও ক্যান্সেলেশনের সাথে ভালো কাজ করলে, এটিকে একটি পুনরাবৃত্তযোগ্য প্যাটার্নে পরিণত করুন। এন্ড টু এন্ড প্রয়োগ করুন: হ্যান্ডলার ডেডলাইন, DB কল, এবং আউটবাউন্ড HTTP। তারপর একই স্ট্রাকচার পরবর্তী এন্ডপয়েন্টে কপি করুন।
বোরিং অংশগুলো কেন্দ্রীভূত করলে আপনি দ্রুত এগোতে পারবেন: একটি বাউন্ডারি টাইমআউট হেল্পার, এমন র্যাপার যা নিশ্চিত করে ctx ডাটাবেস ও HTTP কলগুলোতে পাঠানো হচ্ছে, এবং একটি কনসিস্টেন্ট এরর ম্যাপিং ও লগ ফরম্যাট।
যদি আপনি দ্রুত প্রোটোটাইপ করতে চান, Koder.ai (koder.ai) Go হ্যান্ডলার ও সার্ভিস কলগুলো চ্যাট প্রম্পট থেকে জেনারেট করতে পারে, এবং আপনি সোর্স কোড এক্সপোর্ট করে আপনার নিজস্ব টাইমআউট হেল্পার ও বাজেট প্রয়োগ করতে পারবেন। লক্ষ্য হলো ধারাবাহিকতা: ধীর কলগুলো আগে থামে, এররগুলি দেখতে একই রকম, এবং ডিবাগিং নির্ভর করবে না যে কে এন্ডপয়েন্ট লিখেছে।
ধীর অনুরোধ যখন অপেক্ষা করে থাকে তখন সীমিত রিসোর্স ধরে রাখে: একটি goroutine, বাফার ও রেসপন্স অবজেক্টের জন্য মেমরি, এবং প্রায়ই একটি ডাটাবেস বা HTTP ক্লায়েন্ট সংযোগ। একসাথে অনেক অনুরোধ অপেক্ষা করলে সার্ভিসে কিউ গঠন হয়, লেটেনসি বাড়ে, এবং এমনকি যদি প্রতিটি অনুরোধ আলাদা করে শেষ হতে পারে, সার্ভিস সামগ্রিকভাবে ব্যর্থ হয়ে যেতে পারে।
রাউটার/গেটওয়ে-এ সীমা দিয়ে এবং Go সার্ভারের হ্যান্ডলারেই একটি টাইমআউট সেট করে শুরু করুন। হ্যান্ডলারে একটি নির্দিষ্ট সময়বদ্ধ কনটেক্সট তৈরি করুন এবং সেই ctx প্রতিটি ব্লকিং কল (ডাটাবেস ও বাহ্যিক HTTP) এ পাঠান। ডেডলাইন পূর্ণ হলে দ্রুত একটি কনসিস্টেন্ট টাইমআউট রেসপন্স ফেরত দিন এবং বাতিল-সমর্থন করে এমন ইন-ফ্লাইট কাজ বন্ধ করুন।
context.WithTimeout(parent, d) ব্যবহার করুন যখন আপনি চান “এই সময়ের পরে বন্ধ করুন” — হ্যান্ডলারগুলিতে এটি সবচেয়ে সাধারণ। যদি আপনার কাছে একটি নির্দিষ্ট কাটঅফ সময় থাকে তাহলে context.WithDeadline(parent, t) ব্যবহার করুন। যখন কোনো অভ্যন্তরীণ শর্ত দ্রুত কাজ বন্ধ করবে (যেমন “আমরা ইতোমধ্যে উত্তর পেয়েছি”) তখন context.WithCancel(parent) ব্যবহার করুন।
প্রতিটি ডেরাইভড কনটেক্সট তৈরি করার পর সবসময় defer cancel() রাখুন। এটি টাইমার মুক্ত করে এবং সেই কনটেক্সট-এর শিশুকাজকে স্পষ্টভাবে বন্ধ করার সিগন্যাল দেয়, বিশেষত যদি কোড পথগুলো আগে ফিরে আসে এবং ডেডলাইন নিজে ট্রিগার না করে।
হ্যান্ডলারেই একবার রিকোয়েস্ট কনটেক্সট তৈরি করে তা কল স্ট্যাকের প্রথম আর্গুমেন্ট হিসেবে নীচে পাঠান। context.Background() বা context.TODO() খোঁজার একটি দ্রুত পরীক্ষা করুন; এগুলো প্রায়ই ক্যান্সেলেশন প্রোপাগেশন ভাঙে।
QueryContext, QueryRowContext, ExecContext-এর মতো কনটেক্সট-সক্ষম ডাটাবেস মেথড ব্যবহার করুন। কনটেক্সট শেষ হলে ড্রাইভার Postgres-কে প্রশ্ন বাতিল করার অনুরোধ পাঠায়, যাতে অনুরোধ শেষ হওয়ার পরও আপনি সংযোগ ও সময় খরচ করা বন্ধ করতে পারেন।
বাহ্যিক অনুরোধে প্যারেন্ট রিকোয়েস্ট কনটেক্সট সংযুক্ত করুন http.NewRequestWithContext(ctx, ...) ব্যবহার করে, এবং ট্রান্সপোর্ট লেভেলে টাইমআউট কনফিগার করুন যাতে সংযোগ, TLS হ্যান্ডশেক ও হেডার পাঠানোর সময়ও সুরক্ষা থাকে। ত্রুটি বা নন-200 রেসপন্সে ও রেসপন্স বডি সবসময় Close() করুন যাতে সংযোগ পুল লিক না করে।
একটি মোট বাজেট সার্ভিস-স্তরে প্রথমে ঠিক করুন (উদাহরণ: ইউজার-ফেসিং এন্ডপয়েন্টের জন্য 2 সেকেন্ড)। তারপর প্রতিটি নির্ভরশীল ধাপে ছোট করে ভাগ করুন এবং হ্যান্ডলার ও রেসপন্স এনকোডিংয়ের জন্য একটি ছোট বাফার রাখুন। যদি প্যারেন্ট কনটেক্সটে অল্প সময় বাকি থাকে, এমন কোনো কাজ শুরু করবেন না যা সম্পন্ন করতে পারবে না।
context.DeadlineExceeded-কে সাধারণত 504 Gateway Timeout হিসেবে ম্যাপ করা হয় এবং একটি সংক্ষিপ্ত বার্তা (যেমন “request timed out”) ফেরত দেওয়া হয়। context.Canceled সাধারণত ক্লায়েন্ট ডিসকানেক্ট হওয়ার ইঙ্গিত দেয়; প্রায়শই সেরা কাজ হলো কাজ বন্ধ করে আরও রেসোর্স ব্যয় না করা — সাধারণত কোনো বডি না দিয়ে ছেড়ে দিন।
সর্বাধিক সাধারণ ভুলগুলোর মধ্যে আছে: রিকোয়েস্ট কনটেক্সট বাদ দিয়ে context.Background() ব্যবহার করা, ctx.Done() না চেক করে স্লিপ বা রিট্রাই চালানো, এবং ব্লকিং কলগুলোতে ctx জুড়তে ভুল হওয়া। এছাড়া অনেক জায়গায় আলাদা আলাদা টাইমআউট বসালে টাইটিং ভেঙ্গে যায় এবং সমস্যা বোঝা কঠিন হয়।