API-এর জন্য Go কনটেক্সট টাইমআউট: HTTP হ্যান্ডলার থেকে SQL পর্যন্ত
API-তে Go কনটেক্সট টাইমআউট HTTP হ্যান্ডলার থেকে SQL কল পর্যন্ত ডেডলাইন পাঠাতে সাহায্য করে, স্টাক হওয়া রিকোয়েস্ট প্রতিরোধ করে এবং লোডে সার্ভিস স্থিতিশীল রাখে।

কেন রিকোয়েস্ট স্টাক করে (এবং লোডে কেন এটা সমস্যা)
একটি রিকোয়েস্ট "স্টাক" হয় যখন এটা এমন কিছুর অপেক্ষা করে যা ফিরেও আসে না: একটি ধীর ডাটাবেস কুয়েরি, পুল থেকে ব্লক হওয়া কানেকশন, DNS সমস্যা, বা এমন একটি upstream সার্ভিস যা কল গ্রহণ করে কিন্তু কখনো জবাব দেয় না।
লক্ষণগুলো সহজ দেখায়: কিছু রিকোয়েস্ট চিরস্থায়ীভাবে সময় নেয়, তারপর আরও অনেক রিকোয়েস্ট তাদের পেছনে জমে যায়। প্রায়ই আপনি স্মৃতি বাড়তে দেখবেন, গোরুটিন সংখ্যা বেড়ে যাবে, এবং একটি খোলা কানেকশনের সারি থাকবে যা কখনো শুকায় না।
লোডে, স্টাক হওয়া রিকোয়েস্টের ক্ষতি দ্বি-গুণ। এগুলো ওয়ার্কার들을 ব্যস্ত রাখে, এবং ডাটাবেস কানেকশন ও লকসের মতো সঙ্কীর্ণ রিসোর্স ধরে রাখে। ফলে সাধারণত দ্রুত রিকোয়েস্টগুলো ধীর হয়ে যায়, যা আরও ওভারল্যাপ তৈরি করে, এবং আরও অপেক্ষা বাড়ে।
রিট্রাই ও ট্র্যাফিক স্পাইক এই চক্রকে আরও খারাপ করে। একটি ক্লায়েন্ট টাইমআউট করে এবং রি-ট্রাই করে যখন মূল রিকোয়েস্ট এখনও চলমান থাকে, ফলে আপনি এখন দুইটি রিকোয়েস্টের জন্য অর্থ দেন। অনেক ক্লায়েন্টের মধ্যে এটি গুণ করলে ডাটাবেস ওভারলোড বা কানেকশন সীমা ছোঁয়া সহজ হয়ে যায়, যদিও গড় ট্র্যাফিক ঠিক আছে।
টাইমআউট হলো একটি প্রতিশ্রুতি: "আমরা X এর বেশি অপেক্ষা করব না।" এটা আপনাকে দ্রুত ব্যর্থ হতে ও রিসোর্স মুক্ত করতে সাহায্য করে, কিন্তু কাজকে দ্রুত শেষ করে না।
এটা তাত্ক্ষণিকভাবে কাজ বন্ধ করার নিশ্চয়তাও দেয় না। উদাহরণস্বরূপ, ডাটাবেস কখনো চালিয়ে যেতে পারে, একটি upstream সার্ভিস আপনার ক্যানসেল উপেক্ষা করতে পারে, বা আপনার নিজস্ব কোড ক্যানসেলেশন হলে সেফ না হতে পারে।
টাইমআউট যে জিনিসটি নিশ্চিৎ করে তা হলো: আপনার হ্যান্ডলার অপেক্ষা থামাতে পারে, একটি স্পষ্ট ত্রুটি ফিরিয়ে দিতে পারে, এবং যা ধরে রেখেছিল তা মুক্ত করে দিতে পারে। সীমাবদ্ধ অপেক্ষা-ই কয়েকটি ধীর কলকে পুরো আউটেজে পরিণত হওয়া থেকে রক্ষা করে।
Go কনটেক্সট টাইমআউটের লক্ষ্য হলো একটি শেয়ারড ডেডলাইন এজ থেকে সবচেয়ে গভীর কল পর্যন্ত। HTTP বাউন্ডারিতে একবার সেট করুন, একই ctx সার্ভিস কোড দিয়ে পাস করুন, এবং database/sql কলেও ব্যবহার করুন যাতে ডাটাবেসও কখন অপেক্ষা বন্ধ করতে হবে তা জানে।
Go তে কনটেক্সট সহজ ভাষায়
context.Context হল একটি ছোট অবজেক্ট যা আপনি আপনার কোডের মাধ্যমে পাস করেন যা বর্ণনা করে এখন কি ঘটছে। এটি নিচের প্রশ্নগুলোর উত্তর দেয়: "এই রিকোয়েস্ট কি এখনও বৈধ?", "কখন আমরা ছেড়ে দেব?", এবং "কোন ছোট রিকোয়েস্ট-স্কোপড ভ্যালুগুলো এই কাজের সঙ্গে যাওয়া উচিত?"
বড় সুবিধা হলো সিস্টেমের ধারে (HTTP হ্যান্ডলার) এক সিদ্ধান্ত প্রতিটি ডাউনস্ট্রিম ধাপকে রক্ষা করতে পারে, যদি আপনি একই কনটেক্সট পাস করে যান।
কনটেক্সট কী নিয়ে আসে
কনটেক্সট ব্যবসায়িক ডেটার জায়গা নয়। এটা কন্ট্রোল সিগন্যাল এবং কিছু ছোট রিকোয়েস্ট-স্কোপড উপাত্তের জন্য: ক্যানসেলেশন, একটি ডেডলাইন/টাইমআউট, এবং লগের জন্য রিকোয়েস্ট আইডির মতো ছোট মেটাডেটা।
টাইমআউট বনাম ক্যানসেলেশন সহজ: টাইমআউট ক্যানসেলেশনের একটি কারণ। যদি আপনি ২ সেকেন্ড টাইমআউট দেন, তাহলে ২ সেকেন্ড পরে কনটেক্সট ক্যানসেল হবে। কিন্তু কনটেক্সট আগেভাগে ক্যানসেলও হতে পারে যদি ব্যবহারকারী ট্যাব বন্ধ করে, লোড ব্যালান্সার কানেকশন ছেড়ে দেয়, বা আপনার কোড সিদ্ধান্ত নেয় কাজটি থামাতে হবে।
কনটেক্সট ফাংশন কলের মধ্য দিয়ে একটি স্পষ্ট প্যারামিটার হিসেবে প্রবাহিত হয়, সাধারণত প্রথমটি: func DoThing(ctx context.Context, ...)। উদ্দেশ্যই এটা। প্রতিটি কল সাইটে এটি দেখলে ভুলে যাওয়া কঠিন।
যখন ডেডলাইন শেষ হয়, কনটেক্সটটি পর্যবেক্ষণ করা যেকোনো কাজ দ্রুত থামানো উচিত। উদাহরণস্বরূপ, QueryContext ব্যবহার করে একটি ডাটাবেস কুয়েরি উচিত ত্রুটিসহ আগেভাগে ফিরে আসা, যেমন context deadline exceeded, এবং আপনার হ্যান্ডলার সার্ভার ওয়ার্কার শেষ হওয়ার আগেই টাইমআউট দিয়ে উত্তর পাঠাতে পারে।
একটা ভাল মানসিক মডেল: এক রিকোয়েস্ট, এক কনটেক্সট, সব জায়গায় পাস করুন। যদি রিকোয়েস্ট মারা যায়, কাজটাও মারা যাওয়া উচিত।
HTTP বাউন্ডারিতে স্পষ্ট ডেডলাইন সেট করা
এন্ড-টু-এন্ড টাইমআউট কাজ করাতে চাইলে সিদ্ধান্ত নিন ঘণ্টা কোথা থেকে শুরু। সবচেয়ে নিরাপদ স্থান হলো HTTP এজেই, যাতে প্রতিটি ডাউনস্ট্রিম কল (বিজনেস লজিক, SQL, অন্য সার্ভিস) একই ডেডলাইন উত্তরাধিকার করে।
আপনি ডেডলাইন কয়েক জায়গায় সেট করতে পারেন। সার্ভার-লেভেলের টাইমআউট একটি ভাল বেসলাইন এবং ধীর ক্লায়েন্টদের থেকে রক্ষা করে। মিডলওয়্যার route গ্রুপগুলোর মধ্যে সামঞ্জস্য রাখতে ভাল। হ্যান্ডলারের ভিতরে সেট করাও ঠিক আছে যখন আপনি কিছু স্পষ্ট ও লোকাল চান।
অধিকাংশ API-এর জন্য, মিডলওয়্যার বা হ্যান্ডলারে প্রতি-রিকোয়েস্ট টাইমআউট সবচেয়ে সহজে বোঝা যায়। এগুলো বাস্তবসম্মত রাখুন: ব্যবহারকারীরা ঝুলে থাকা রিকোয়েস্টের চেয়ে দ্রুত, স্পষ্ট ব্যর্থতা পছন্দ করে। অনেক টিম রিডসের জন্য সংক্ষিপ্ত বাজেট (যেমন 1–2s) এবং রাইটসের জন্য একটু দীর্ঘ (3–10s) ব্যবহার করে, endpoint-এ কি হবে তার উপর নির্ভর করে।
নিচে একটি সিম্পল হ্যান্ডলার প্যাটার্ন দেয়া আছে:
func (s *Server) getReport(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
report, err := s.reports.Generate(ctx, r.URL.Query().Get("id"))
if err != nil {
http.Error(w, err.Error(), http.StatusGatewayTimeout)
return
}
json.NewEncoder(w).Encode(report)
}
এই কার্যকর রাখার দুইটি নিয়ম:
- সর্বদা
cancel()কল করুন যাতে টাইমার ও রিসোর্স দ্রুত মুক্ত হয়। - হ্যান্ডলার ভিতরে কখনো
context.Background()বাcontext.TODO()দিয়ে রিকোয়েস্ট কনটেক্সট প্রতিস্থাপন করবেন না। এতে চেইন ভেঙে যায়, এবং আপনার ডাটাবেস কল ও আউটবাউন্ড রিকোয়েস্ট ক্লায়েন্ট চলে গেলেও অনন্তকাল চলতে পারে।
কনটেক্সট আপনার কোডবেস জুড়ে প্রোপাগেট করা
একবার আপনি HTTP বাউন্ডারিতে ডেডলাইন সেট করলে, আসল কাজ হলো নিশ্চিত করা সেই একই ডেডলাইন প্রতিটি স্তরে পৌঁছায় যা ব্লক করতে পারে। ধারণাটি এক ঘণ্টা, হ্যান্ডলার, সার্ভিস কোড, এবং নেটওয়ার্ক বা ডিস্ক স্পর্শকারী যেকোনো কিছু শেয়ার করে।
একটি সহজ নিয়ম সবকিছু কনসিস্টেন্ট রাখে: যে কোনো ফাংশন যা অপেক্ষা করতে পারে তাকে context.Context গ্রহণ করা উচিত, এবং এটি প্রথম প্যারামিটার হওয়া উচিত। এতে কল সাইটগুলোতে স্পষ্ট হয়, এবং এটি অভ্যাসে পরিণত হয়।
ব্যবহারিক সিগনেচার প্যাটার্ন
সার্ভিস এবং রিপোজিটরিদের জন্য DoThing(ctx context.Context, ...) মতো সিগনেচার প্রেফার করুন। কনটেক্সটকে struct-এ লুকানো বা নীচু স্তরে context.Background() দিয়ে পুনরায় তৈরি করা এজ কলে করা ডেডলাইন চুপচাপ হারিয়ে ফেলে — এড়ান।
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if err := h.svc.CreateOrder(ctx, r.Body); err != nil {
// map context errors to a clear client response elsewhere
http.Error(w, err.Error(), http.StatusRequestTimeout)
return
}
}
func (s *Service) CreateOrder(ctx context.Context, body io.Reader) error {
// parsing or validation can still respect cancellation
select {
case <-ctx.Done():
return ctx.Err()
default:
}
return s.repo.InsertOrder(ctx, /* data */)
}
আগেভাগে এক্সিটগুলি ক্লিনলি হ্যান্ডল করা
ctx.Done()-কে একটি স্বাভাবিক কন্ট্রোল পথ হিসেবে বিবেচনা করুন। দুটি অভ্যাস সাহায্য করে:
- ব্যয়বহুল কাজ শুরু করার আগে এবং লম্বা লুপগুলোতে পরে
ctx.Err()চেক করুন। ctx.Err()অপরিবর্তিত রেখে উপরে পাঠান, যাতে হ্যান্ডলার দ্রুত উত্তর দিতে পারে এবং রিসোর্স নষ্ট করতে না থাকে।
যখন প্রতিটি স্তর একই ctx পাস করে, একটি একক টাইমআউট পার্সিং, বিজনেস লজিক, এবং ডাটাবেস অপেক্ষাকে একসাথে কেটে দিতে পারে।
database/sql কুয়েরিগুলোতে ডেডলাইন প্রয়োগ করা
আপনার HTTP হ্যান্ডলার যখন ডেডলাইন রাখে, তখন নিশ্চিত করুন ডাটাবেস কাজটাও তা শোনে। database/sql-এর ক্ষেত্রে, এর মানে হচ্ছে প্রতিবার context-aware মেথড ব্যবহার করা। আপনি যদি Query() বা Exec() কনটেক্সট ছাড়া কল করেন, আপনার API ধীরে ধীরে একটি ধীর কুয়েরির অপেক্ষায় থাকতে পারে এমনকি ক্লায়েন্ট আগেই ছেড়ে গিয়েছে।
নিয়মিত ব্যবহার করুন: db.QueryContext, db.QueryRowContext, db.ExecContext, এবং db.PrepareContext (তারপর ফিরে পাওয়া স্টেটমেন্টে QueryContext/ExecContext ব্যবহার করুন)।
func (s *Store) GetUser(ctx context.Context, id int64) (*User, error) {
row := s.db.QueryRowContext(ctx,
`SELECT id, email FROM users WHERE id = $1`, id,
)
var u User
if err := row.Scan(&u.ID, &u.Email); err != nil {
return nil, err
}
return &u, nil
}
func (s *Store) UpdateEmail(ctx context.Context, id int64, email string) error {
_, err := s.db.ExecContext(ctx,
`UPDATE users SET email = $1 WHERE id = $2`, email, id,
)
return err
}
দুইটি সহজ কিন্তু যাচাই করা দরকার এমন জিনিস আছে।
প্রথমত, আপনার SQL ড্রাইভার কনটেক্সট ক্যানসেলেশন সম্মান করে কিনা নিশ্চিত করুন। অনেক ড্রাইভার করে, কিন্তু আপনার স্ট্যাকে একটি ইচ্ছাকৃত ধীর কুয়েরি চালিয়ে পরীক্ষা করে দেখুন এটি ডেডলাইন পেরোলে দ্রুত বাতিল হয় কিনা।
দ্বিতীয়ত, ডাটাবেস-সাইড টাইমআউট একটি ব্যাকস্টপ হিসেবে বিবেচনা করুন। উদাহরণস্বরূপ, Postgres প্রতি স্টেটমেন্টের জন্য একটি সীমা আরোপ করতে পারে (সাধারণত statement timeout নামে)। এটা ডাটাবেসকে রক্ষা করে এমনকি অ্যাপ বাগ কোথাও কনটেক্সট পাস করতে ভুল করলেও।
যখন কোনো অপারেশন টাইমআউটের কারণে থামে, এটাকে সাধারণ SQL এরর থেকে আলাদা করে হ্যান্ডল করুন। errors.Is(err, context.DeadlineExceeded) এবং errors.Is(err, context.Canceled) চেক করুন এবং একটি স্পষ্ট রেসপন্স (যেমন 504) ফেরত দিন, সাধারণভাবে "ডাটাবেস ভাঙা" হিসেবে না দেখিয়ে। যদি আপনি Go ব্যাকএন্ড জেনারেট করেন (উদাহরণস্বরূপ AppMaster দিয়ে), এই এরর পাথগুলো আলাদা রাখলে লগ ও রিট্রাই বোঝা সহজ হয়।
ডাউনস্ট্রিম কল: HTTP ক্লায়েন্ট, ক্যাশ, ও অন্যান্য সার্ভিস
হ্যান্ডলার ও SQL কুয়েরি কনটেক্সট সম্মান করলেও, একটি অনুরোধ তখনো হ্যাং করতে পারে যদি ডাউনস্ট্রিম কল অনন্তকাল অপেক্ষা করে। লোডে, কিছু স্টাক হওয়া গোরুটিন জমে যায়, কানেকশন পুলগুলো খেয়ে নেয়, এবং ছোট ধীরগতি একটি পূর্ণ আউটেজে পরিণত করে। সমাধান হলো ধারাবাহিক প্রোপাগেশন প্লাস একটি কঠোর ব্যাকস্টপ।
আউটবাউন্ড HTTP
অন্য API-কে কল করার সময় একই কনটেক্সট দিয়ে অনুরোধ তৈরি করুন যাতে ডেডলাইন ও ক্যানসেলেশন স্বয়ংক্রিয়ভাবে প্রবাহিত হয়।
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { /* handle */ }
resp, err := httpClient.Do(req)
কেবল কনটেক্সটের উপর নির্ভর করবেন না। কোড ভুলভাবে background context ব্যবহার করলে বা DNS/TLS/idle কানেকশন আটকে গেলে রক্ষণ করার জন্য HTTP ক্লায়েন্ট এবং ট্রান্সপোর্ট কনফিগার করুন। http.Client.Timeout হিসেবে পুরো কলের উপর একটি উপরের সীমানা দিন, ট্রান্সপোর্ট টাইমআউট (dial, TLS handshake, response header) সেট করুন, এবং প্রতি রিকোয়েস্টে নতুন ক্লায়েন্ট তৈরি না করে একটি রিইউজ করা ক্লায়েন্ট ব্যবহার করুন।
ক্যাশ ও কিউ
ক্যাশ, মেসেজ ব্রোকার, এবং RPC ক্লায়েন্টদেরও নিজের অপেক্ষার পয়েন্ট থাকে: কানেকশন অর্জন করা, একটি রেপ্লাই পেতে অপেক্ষা করা, পূর্ণ কিউতে ব্লক হওয়া, বা লকে আটকা পড়া। নিশ্চিত করুন সেই অপারেশনগুলো ctx গ্রহণ করে, এবং যেখানে লাইব্রেরি-লেভেল টাইমআউট আছে সেগুলোও ব্যবহার করুন।
প্রায়োগিক নিয়ম: যদি ব্যবহারকারীর রিকোয়েস্টের কাছে 800ms বাকি থাকে, তাহলে 2 সেকেন্ড নেয় এমন ডাউনস্ট্রিম কল কখনো শুরু করবেন না। বাদ দিন, degrade করুন, বা আংশিক রেসপন্স দিন।
টাইমআউট আপনার API-এর জন্য আগে থেকে সিদ্ধান্ত নিন। কখনো কখনো সঠিক উত্তর দ্রুত একটি ত্রুটি দেওয়া। কখনো এটি ঐচ্ছিক ফিল্ডের জন্য আংশিক ডেটা। কখনো এটি ক্যাশ থেকে স্টেইল ডেটা, স্পষ্টভাবে মার্ক করে দেয়া।
যদি আপনি Go ব্যাকএন্ড (সহজভাবে নো-কোড জেনারেটেড) নির্মাণ করেন, এই পার্থক্য হলো "টাইমআউট আছে" বনাম "টাইমআউট কনসিস্টেন্টলি সিস্টেমকে রক্ষা করে" ট্রাফিক স্পাইকে।
ধাপে ধাপে: একটি API-কে এন্ড-টু-এন্ড টাইমআউট ব্যবহার করে রিফ্যাক্টর করা
টাইমআউটের জন্য রিফ্যাক্টর করা মূলত একটি অভ্যাসে নামিয়ে আনা: HTTP এজ থেকে একই context.Context প্রতিটি ব্লকিং কল পর্যন্ত পাস করা।
প্রাকটিক্যাল উপায়টি উপরে থেকে নিচে কাজ করা:
- আপনার হ্যান্ডলার ও কোর সার্ভিস মেথডগুলোকে
ctx context.Contextগ্রহণ করতে বদলান। - প্রতিটি DB কল আপডেট করুন যাতে
QueryContextবাExecContextব্যবহার করে। - একই কথা বাইরে কলগুলোর জন্য করুন (HTTP ক্লায়েন্ট, ক্যাশ, কিউ)। যদি কোনো লাইব্রেরি
ctxনেয় না, ওভারল্যাপ করুন বা প্রতিস্থাপন করুন। - সিদ্ধান্ত নিন কে টাইমআউটের মালিক। সাধারণ নিয়ম: হ্যান্ডলার মোট ডেডলাইন সেট করে; নিচু স্তরগুলো শুধুমাত্র নির্দিষ্ট অপারেশনের জন্য ছোটতর ডেডলাইন সেট করবে।
- এজ-এ এররগুলো প্রেডিক্টেবল করুন:
context.DeadlineExceededওcontext.Canceled-কে স্পষ্ট HTTP রেসপন্সে ম্যাপ করুন।
নিচে স্তরগুলোর আকৃতি যা আপনি চান:
func (h *Handler) GetOrder(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
order, err := h.svc.GetOrder(ctx, r.PathValue("id"))
if errors.Is(err, context.DeadlineExceeded) {
http.Error(w, "request timed out", http.StatusGatewayTimeout)
return
}
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
_ = json.NewEncoder(w).Encode(order)
}
func (r *Repo) GetOrder(ctx context.Context, id string) (Order, error) {
row := r.db.QueryRowContext(ctx, `SELECT id,total FROM orders WHERE id=$1`, id)
// scan...
}
টাইমআউট মানগুলো নীরস ও কনসিস্টেন্ট হওয়া উচিত। যদি হ্যান্ডলার কাছে মোট 2 সেকেন্ড থাকে, DB কুয়েরিগুলো 1 সেকেন্ডের মধ্যে রাখুন যাতে JSON এনকোডিং ও অন্যান্য কাজের জন্য জায়গা থাকে।
এটা কাজ করে কিনা প্রমাণ করতে একটি টেস্ট যোগ করুন যা ইচ্ছাকৃতভাবে টাইমআউট জোর করে। একটি সহজ পন্থা হলো একটি ফেক রিপো মেথড যা ctx.Done() পর্যন্ত ব্লক করে এবং তারপর ctx.Err() ফেরত দেয়। আপনার টেস্টটি নিশ্চিত করবে হ্যান্ডলার দ্রুত 504 ফেরত দেয়, ফেক ডিলে-র পরে নয়।
যদি আপনি AppMaster-এর মতো জেনারেটর দিয়ে Go ব্যাকএন্ড তৈরি করেন, নিয়ম একই: এক রিকোয়েস্ট কনটেক্সট, সার্বত্রিকভাবে থ্রেড করা এবং ডেডলাইনের পরিষ্কার মালিকানা।
অবজারভেবিলিটি: টাইমআউট কাজ করছে তা প্রমাণ করা
টাইমআউট তখনই সাহায্য করে যখন আপনি এগুলো ঘটতে দেখেন। লক্ষ্য সহজ: প্রতিটি রিকোয়েস্টের একটি ডেডলাইন আছে, এবং যখন তা ব্যর্থ হয় আপনি বলতে পারেন সময় কোথায় গেল।
লগগুলো শুরু করুন যা নিরাপদ ও কাজে লাগে। পূর্ণ রিকোয়েস্ট বডি লিপিবদ্ধ করার বদলে যথেষ্ট তথ্য লগ করুন যাতে ডটগুলো সংযুক্ত করা যায় এবং ধীর পথগুলো শনাক্ত করা যায়: রিকোয়েস্ট ID (বা ট্রেস ID), কী সময়সীমা সেট আছে এবং কী পরিমাণ সময় বাকি আছে মূল পয়েন্টগুলোতে, অপারেশন নাম (হ্যান্ডলার, SQL কুয়েরি নাম, আউটবাউন্ড কল নাম), এবং ফলাফল ক্যাটেগরি (ok, timeout, canceled, other error)।
কিছু ফোকাসড মেট্রিক যোগ করুন যাতে লোডে আচরণ স্পষ্ট হয়:
- এন্ডপয়েন্ট ও ডিপেন্ডেন্সি অনুযায়ী টাইমআউট গোনা
- রিকোয়েস্ট ল্যাটেন্সি (p50/p95/p99)
- ইন-ফ্লাইট রিকোয়েস্ট
- ডাটাবেস কুয়েরি ল্যাটেন্সি (p95/p99)
- এরর রেট টাইপ অনুযায়ী বিভক্ত
যখন আপনি এরর হ্যান্ডল করেন, সেগুলোকে সঠিকভাবে ট্যাগ করুন। context.DeadlineExceeded সাধারণত মানে হলো আপনি আপনার বাজেট ছাড়িয়ে গেছেন। context.Canceled প্রায়ই মানে ক্লায়েন্ট গেল বা upstream টাইমআউট আগে ফায়ার করেছে। এগুলো আলাদা রাখুন কারণ সমাধান ভিন্ন হয়।
ট্রেসিং: কোথায় সময় খাচ্ছে খুঁজে বের করা
ট্রেসিং স্প্যানগুলো একই কনটেক্সট অনুসরণ করা উচিত HTTP হ্যান্ডলার থেকে database/sql কলগুলো পর্যন্ত যেমন QueryContext। উদাহরণস্বরূপ, একটি রিকোয়েস্ট ২ সেকেন্ডে টাইমআউট হয় এবং ট্রেস দেখায় 1.8 সেকেন্ড ডাটাবেস কানেকশন অপেক্ষায় গেছে — এটি পুল সাইজ বা ধীর ট্রানজ্যাকশনের দিকে ইঙ্গিত করে, কুয়েরি টেক্সট নয়।
আপনি যদি এই জন্য একটি ইন্টারনাল ড্যাশবোর্ড তৈরি করেন (রুট অনুযায়ী টাইমআউট, টপ ধীর কুয়েরি), একটি নো-কোড টুল যেমন AppMaster আপনাকে দ্রুত এটি চালু করতে সাহায্য করতে পারে যাতে অবজারভেবিলিটি আলাদা প্রকল্প না হয়।
সাধারণ ভুল যা আপনার টাইমআউটকে বাতিল করে
"এখনো কখনো হ্যাং করে" জাতীয় বাগগুলো বেশিরভাগই কিছু ছোট ভুল থেকেই হয়।
- মাঝে মাঝে ঘড়ি রিসেট করা। একটি হ্যান্ডলার 2s ডেডলাইন সেট করে, কিন্তু রিপো নিজের নতুন টাইমআউট তৈরি করে (অথবা কোনো টাইমআউটই নেই)। এখন ডাটাবেস ক্লায়েন্ট গেলেও চালিয়ে যেতে পারে। ইনকামিং
ctxপাস করুন এবং শুধুমাত্র স্পষ্ট কারণে এটি কঠিন করুন। - গোরুটিন শুরু করা যা কখনো থামে না।
context.Background()দিয়ে কাজ spawn করা মানে তা রিকোয়েস্ট ক্যানসেল হলে চলতে থাকবে। গোরুটিনে রিকোয়েস্টctxপাস করুন এবংselectকরেctx.Done()-এ reaction দিন। - রিয়াল ট্রাফিকের জন্য অত্যন্ত সংক্ষিপ্ত ডেডলাইন। 50ms টাইমআউট আপনার ল্যাপটপে কাজ করলেও প্রোডাকশনে ছোট স্পাইকে ফেলবে, রিট্রাই ও লোড বাড়াবে, এবং ছোট আউটেজ তৈরি করবে। টাইমআউটগুলি সাধারণ লেটেন্সি এবং হেডরুম ভিত্তিতে নিন।
- সত্যিকার এরর লুকানো।
context.DeadlineExceededকে generic 500 হিসেবে ট্রিট করলে ডিবাগিং ও ক্লায়েন্ট আচরণ খারাপ হয়। এটি স্পষ্ট টাইমআউট রেসপন্সে ম্যাপ করুন এবং "ক্লায়েন্ট ক্যানসেল করেছে" বনাম "টাইমআউট হয়েছে" আলাদা লগ করুন। - আগেভাগে এক্সিটে রিসোর্স খোলা রাখা। আপনি যদি আগে ফেরত দেন, নিশ্চিত করুন এখনও
defer rows.Close()রয়েছে এবংcontext.WithTimeoutথেকে cancel ফাংশন কল করা হচ্ছে। লিক করা rows বা দীর্ঘস্থায়ী কাজ লোডে কানেকশন শেষ করে দিতে পারে।
একটি দ্রুত উদাহরণ: একটি endpoint একটি রিপোর্ট কুয়েরি ট্রিগার করে। যদি ব্যবহারকারী ট্যাব বন্ধ করে দেয়, হ্যান্ডলার ctx ক্যানসেল হবে। যদি আপনার SQL কল নতুন background context ব্যবহার করে, কুয়েরি তখনো চলবে, একটি কানেকশন নিয়ে থাকবে এবং সবাইকে ধীর করে দেবে। একই ctx QueryContext-এ পাস করলে ডাটাবেস কল ইন্টারাপ্ট হবে এবং সিস্টেম দ্রুত পুনরুদ্ধার করবে।
নির্ভরযোগ্য টাইমআউট আচরণের দ্রুত চেকলিস্ট
টাইমআউট কেবল তখনই সাহায্য করে যখন এগুলো কনসিস্টেন্ট। একটি একক মিস করা কল একটি গোরুটিন ব্যস্ত রাখতে পারে, একটি DB কানেকশন ধরে রাখতে পারে, এবং পরবর্তী রিকোয়েস্টগুলোকে ধীর করে দিতে পারে।
- এজেই একটি স্পষ্ট ডেডলাইন সেট করুন (সাধারণত HTTP হ্যান্ডলার)। রিকোয়েস্টের ভিতরের সবকিছু এটি উত্তরাধিকার করবে।
- একই
ctxআপনার সার্ভিস ও রিপোজিটরি লেয়ারে পাস করুন। রিকোয়েস্ট কোডেcontext.Background()এড়ান। - সব জায়গায় context-aware DB মেথড ব্যবহার করুন:
QueryContext,QueryRowContext, এবংExecContext। - আউটবাউন্ড কলগুলিতে একই
ctxসংযুক্ত করুন (HTTP ক্লায়েন্ট, ক্যাশ, কিউ)। যদি child context তৈরি করেন, তা ছোটতর রাখুন, দীর্ঘ নয়। - ক্যানসেলেশন ও টাইমআউটগুলি কনসিস্টেন্টলি হ্যান্ডল করুন: একটি পরিষ্কার এরর রিটার্ন করুন, কাজ থামান, এবং ক্যানসেল হওয়া রিকোয়েস্টের ভিতরে রিট্রাই লুপ এড়ান।
তারপর, চাপের মধ্যে আচরণ যাচাই করুন। একটি টাইমআউট যা ট্রিগার হয় কিন্তু রিসোর্স দ্রুত মুক্ত করে না তাও নির্ভরশীলতাকে ক্ষতি করে।
ড্যাশবোর্ডগুলোকে টাইমআউট স্পষ্টভাবে দেখাতে হবে, গড়ের ভেতরে লুকিয়ে না রাখা। কয়েকটি সিগনাল ট্র্যাক করুন যা উত্তর দেয় "ডেডলাইন সত্যিই প্রয়োগ হচ্ছে কি না?": রিকোয়েস্ট টাইমআউট ও DB টাইমআউট (আলাদা), লেটেন্সি পারসেন্টাইল (p95, p99), DB পুল স্ট্যাটস (in-use connections, wait count, wait duration), এবং এরর কারণের বিভাজন (context deadline exceeded বনাম অন্যান্য ফেইলিউর)।
যদি আপনি AppMaster-এর মতো প্ল্যাটফর্মে ইন্টারনাল টুল বানান, তখন একই চেকলিস্ট আপনি যে কোন Go সার্ভিসে প্রয়োগ করবেন: বাউন্ডারিতে ডেডলাইন নির্ধারণ, তা প্রোপাগেট করা, এবং মেট্রিকে নিশ্চিত করা যে স্টাক হওয়া রিকোয়েস্ট দ্রুত ফেল করা হচ্ছে না ধীরে ধীরে পাইলআপ হচ্ছে।
উদাহরণ পরিস্থিতি ও পরবর্তী ধাপ
একটি সাধারণ জায়গা যেখানে এটি উপকারী হয় তা হলো একটি সার্চ এন্ডপয়েন্ট। কল্পনা করুন GET /search?q=printer ডাটাবেসে একটি বড় রিপোর্ট কুয়েরির কারণে ধীর হয়ে যায়। ডেডলাইন না থাকলে, প্রতিটি ইনকমিং রিকোয়েস্ট দীর্ঘ SQL কুয়েরির অপেক্ষায় বসে থাকবে। লোডে, ঐ স্টাক হওয়া রিকোয়েস্টগুলো জমে যাবে, ওয়ার্কার গোরুটিন ও কানেকশন আটকে দেবে, এবং পুরো API জ্যাম হয়ে যাবে।
HTTP হ্যান্ডলারে একটি স্পষ্ট ডেডলাইন এবং একই ctx রিপোজিটরিতে পাস করলে, বাজেট শেষ হলে সিস্টেম অপেক্ষা বন্ধ করে দেয়। ডেডলাইন মাথা দিলে ডাটাবেস ড্রাইভার (যদি সমর্থন করে) কুয়েরি বাতিল করে, হ্যান্ডলার ফিরে আসে, এবং সার্ভার নতুন রিকোয়েস্ট সার্ভ করা চালিয়ে যেতে পারে পুরনোগুলোর পিছনে আটকে না থেকে।
ব্যবহারকারীর দৃশ্যে আচরণও উন্নত হয় ভাঙলে। 30–120 সেকেন্ড চালু থেকে হ্যাং করার বদলে ক্লায়েন্ট দ্রুত ও নির্ধারিত ত্রুটি (সাধারণত 504 বা 503 ছোট বার্তা সহ "request timed out") পায়। আরও গুরুত্বপূর্ণ হলো সিস্টেম দ্রুত পুনরুদ্ধার করে কারণ নতুন রিকোয়েস্ট পুরনোগুলোর পেছনে ব্লক করে না।
পরবর্তী পদক্ষেপগুলো যাতে এটি সব এন্ডপয়েন্ট ও টিম জুড়ে স্থায়ী হয়:
- এন্ডপয়েন্ট টাইপ অনুযায়ী স্ট্যান্ডার্ড টাইমআউট বেছে নিন (search বনাম writes বনাম exports)।
- কোড রিভিউতে
QueryContextওExecContextবাধ্যতামূলক করুন। - এজ-এ টাইমআউট এররগুলো স্পষ্ট রাখুন (সুস্পষ্ট স্ট্যাটাস কোড, সহজ বার্তা)।
- টাইমআউট ও ক্যানসেলেশনের জন্য মেট্রিক যোগ করুন যাতে রিগ্রেশন দ্রুত ধরা পড়ে।
- একটি হেল্পার লিখুন যা কনটেক্সট ক্রিয়েশন ও লগিং র্যাপ করে যাতে প্রতিটি হ্যান্ডলার একই ভাবে আচরণ করে।
আপনি যদি AppMaster দিয়ে সার্ভিস ও ইন্টারনাল টুল তৈরি করেন, আপনি এই টাইমআউট নিয়মগুলো জেনারেটেড Go ব্যাকএন্ড, API ইন্টিগ্রেশন, ও ড্যাশবোর্ডে এক জায়গায় ধারাবাহিকভাবে প্রয়োগ করতে পারবেন। AppMaster appmaster.io-তে উপলব্ধ (no-code, with real Go source code generation), তাই যখন আপনি কাস্টম কন্ট্রোল চাইবেন তখনও এটি প্র্যাকটিক্যাল হতে পারে।
প্রশ্নোত্তর
একটি রিকোয়েস্ট তখনই “স্টাক” হয়ে যায় যখন সেটি এমন কিছু অপেক্ষা করে যা ফিরত এসেছে না — ধীর SQL কুয়েরি, ব্লক হওয়া পুল কানেকশন, DNS সমস্যা, বা এমন একটি upstream সার্ভিস যা কল গ্রহণ করে কিন্তু কখনো জবাব দেয় না। লোডের সময় স্টাক হওয়া রিকোয়েস্টগুলো জমে যায়, ওয়ার্কার ও কানেকশন আটকে দেয়, এবং ছোট ধীরগতি থেকে বড় আউটেজ তৈরি করে।
সার্ভারের ধারে (HTTP boundary) মোট সময়সীমা নির্ধারণ করাই safest: হ্যান্ডলার বা মিডলওয়্যার থেকে সেই একই ctx নিচে সকল স্তরে পাঠান। একজোট ডেডলাইনই কম্পোনেন্টগুলোকে দীর্ঘ সময়ে সম্পদ ধরে রাখার থেকে রক্ষা করে।
ctx, cancel := context.WithTimeout(r.Context(), d) ব্যবহার করুন এবং হ্যান্ডলার বা মিডলওয়্যারে সবসময় defer cancel() কল করুন। cancel() টাইমার ও রিসোর্স মুক্ত করে, বিশেষত রিকোয়েস্টটি আগে শেষ হলে তা দ্রুত থামায়।
সবচেয়ে বড় ভুল হল রিকোয়েস্ট কোডে context.Background() বা context.TODO() দিয়ে মূল কনটেক্সট বদলে ফেলা। এতে cancellation ও deadlines ভেঙে যায় এবং downstream কাজ (যেমন SQL বা আউটবাউন্ড HTTP) ক্লায়েন্ট গেলেও চলতে থাকে।
context.DeadlineExceeded এবং context.Canceled কে স্বাভাবিক কন্ট্রোল ফলাফল হিসেবে দেখুন এবং উপর দিকে অপরিবর্তিতভাবে পাঠান। এজ-এ এগুলোকে স্পষ্ট HTTP রেসপন্স (প্রায়শই টাইমআউটের জন্য 504) হিসেবে ম্যাপ করুন যাতে ক্লায়েন্ট অজ্ঞাত 500-এর ওপর নির্বিচারে রিট্রাই না করে।
প্রতি স্থানে কনটেক্সট-সক্ষম মেথড ব্যবহার করুন: QueryContext, QueryRowContext, ExecContext, এবং PrepareContext। Query() বা Exec() কনটেক্সট ছাড়া কল করলে হ্যান্ডলার টাইমআউট হলেও ডাটাবেস কল আপনার গোরুটিন ব্লক করে কানেকশন ধরে রাখতে পারে।
অনেক ড্রাইভার context cancellation সম্মান করে, কিন্তু আপনার স্ট্যাকে পরীক্ষা করে দেখুন: ইচ্ছাকৃতভাবে ধীর কুয়েরি চালিয়ে নিশ্চিত করুন ডেডলাইন পেরোল্যাপ হলে তা দ্রুত বাতিল হয়। এছাড়া ব্যাকস্টপ হিসেবে ডাটাবেস-সাইড স্টেটমেন্ট টাইমআউট সেট করা ভালো।
আউটবাউন্ড অনুরোধ তৈরি করুন http.NewRequestWithContext(ctx, ...) দিয়ে যাতে একই ডেডলাইন ও cancellation স্বয়ংক্রিয়ভাবে প্রয়োগ হয়। পাশাপাশি http.Client.Timeout ও ট্রান্সপোর্ট টাইমআউট কনফিগার করুন যাতে কোনো নিচু স্তরের স্টল হলে বা কেউ background context ব্যবহার করলে এক কঠোর সীমা থাকে।
নীচের স্তরগুলো কেবল ডেডলাইন বাড়াবে না। যদি দরকার হয়, সেখানে ছোটতর child context দিন — কিন্তু কখনই মূল রিকোয়েস্টের সময়সীমা ছাপিয়ে যান না। রিকোয়েস্টের কাছে যদি মাত্র সামান্য সময় থাকে, তাহলে অপশনাল ডাউনস্ট্রিম কল বাদ দিন, আংশিক রেসপন্স দিন, বা দ্রুত ব্যর্থ করুন।
এন্ড-টু-এন্ড টাইমআউট কাজ করছে কিনা দেখতে টাইমআউট ও ক্যানসেলেশন আলাদা করে মনিটর করুন, এন্ডপয়েন্ট ও ডিপেন্ডেন্সি অনুযায়ী। ট্রেসিংয়ে একই কনটেক্সট ফলো করুন যাতে handler, আউটবাউন্ড কল, এবং QueryContext-এ কোথায় সময় গেল সেটা দেখা যায়।


