CodeGym /Các khóa học /ChatGPT Apps /Tác vụ bất đồng bộ: hàng đợi, worker, gửi lại (retry)

Tác vụ bất đồng bộ: hàng đợi, worker, gửi lại (retry)

ChatGPT Apps
Mức độ , Bài học
Có sẵn

1. Vì sao cần tác vụ bất đồng bộ trong ChatGPT App

Nếu thế giới hoàn hảo, bất kỳ MCP tool nào của bạn cũng hoàn thành trong vài trăm mili‑giây. Nhưng đời thực mọi thứ thú vị đều dài và nặng:

  • phân tích một file CSV lớn với lịch sử mua sắm của người dùng;
  • tổng hợp dữ liệu từ nhiều API bên ngoài, mỗi cái lúc thì “ngủ”, lúc thì trả về 503;
  • xây dựng khuyến nghị phức tạp với nhiều bước trung gian;
  • sinh báo cáo và bản trình bày lớn.

Nếu cố nhồi tất cả vào một lần tool‑call đồng bộ, bạn sẽ gặp ba vấn đề.

Thứ nhất, timeout. Phiên ChatGPT, hạ tầng HTTP, MCP client — tất cả không được thiết kế để chờ “trong năm phút nữa”. Máy chủ giữ kết nối quá lâu sẽ trông như “đơ” cả đối với ChatGPT lẫn người dùng.

Thứ hai, quản lý tải. Nếu một trăm người dùng đồng thời chạy “siêu phân tích quà tặng cho năm mới”, bạn không muốn MCP server giữ đồng bộ một trăm tác vụ dài ngay trong các luồng HTTP. Bạn cần một lớp đệm biết hấp thụ đột biến, xếp tác vụ vào hàng đợi và xử lý bằng nhiều worker.

Thứ ba, UX. Người dùng bấm nút trong widget GiftGenius rồi nhìn một spinner suốt 40 giây — cảm giác chẳng khác ngân hàng trực tuyến ngày xưa. Mô hình “trả lời nhanh + tiến độ + có thể hủy” dễ chịu hơn nhiều.

Những vấn đề này được giải bằng sơ đồ chung: “khởi chạy → hàng đợi → nền → sự kiện”.

2. Kiến trúc cơ bản async‑job trong bối cảnh MCP

Lấy ví dụ GiftGenius. Giả sử có kịch bản nặng mới: “Phân tích sâu sở thích dựa trên lịch sử mua sắm và mạng xã hội của bạn bè”. Thứ này có thể chạy vài phút, vì vậy:

  1. Công cụ MCP (tool) nhận tham số yêu cầu từ mô hình.
  2. Thay vì tính toán ngay, nó tạo bản ghi Job trong CSDL.
  3. Đưa tác vụ vào hàng đợi.
  4. Trả lời ChatGPT ngay: “Đã khởi chạy phân tích, đây là jobId”.
  5. Worker nền lấy tác vụ từ hàng đợi, làm việc nặng, trong quá trình gửi sự kiện MCP job.progressjob.partial, cuối cùng — job.completed hoặc job.failed.

Về mặt kiến trúc, trông sẽ như sau:

flowchart LR
    subgraph ChatGPT
      U[Người dùng] --> GPT[Model + ChatGPT UI]
    end

    GPT -->|call_tool analyze_preferences| MCP[Máy chủ MCP]

    subgraph Backend
      MCP -->|tạo Job| DB[(CSDL tác vụ)]
      MCP -->|enqueue| Q[Hàng đợi]
      W[Worker] -->|take job| Q
      W -->|update status/progress| DB
      W -->|MCP events: job.progress/job.completed| MCP
    end

    MCP -->|SSE events| GPT

Điểm quan trọng: MCP server không nhất thiết là một khối đơn. Thường nó đóng vai lớp facade trước hạ tầng async nội bộ của bạn: nhận tool‑call, tạo job và gửi sự kiện, còn công việc nặng do các tiến trình worker riêng thực hiện.

3. Mô hình dữ liệu cho tác vụ bất đồng bộ

Bắt đầu với mô hình đơn giản Job. Ta sẽ dùng TypeScript và Node/MCP server giả định để bạn thấy rõ nó khớp với stack của mình thế nào.

Mô hình tối giản trong bộ nhớ/CSDL có thể như sau:

// openai/jobs/model.ts
export type JobStatus =
  | 'pending'
  | 'in_progress'
  | 'completed'
  | 'failed'
  | 'canceled';

export interface GiftJob {
  id: string;                // jobId
  type: 'deep_gift_analysis';
  status: JobStatus;
  payload: {
    recipientProfile: string;  // văn bản/ID hồ sơ
    budget: number;
  };
  result?: unknown;          // khuyến nghị cuối cùng
  error?: string;            // lý do lỗi
  attempts: number;          // số lần đã thử thực thi
  createdAt: Date;
  updatedAt: Date;
}

Trong dự án thực tế bạn sẽ lưu GiftJob ở Postgres, DynamoDB, Firestore hay nơi khác, nhưng cho bài giảng này các trường quan trọng là:

  • status — trạng thái hiện tại của tác vụ, cũng phản ánh trong sự kiện và UX;
  • attempts — bộ đếm cho retry;
  • error — cho log và debug;
  • payload — dữ liệu đầu vào mà worker dùng để xử lý.

4. Công cụ MCP tạo async‑job

Giả sử có công cụ start_deep_analysis. Trước đây nó có thể làm hết đồng bộ, còn giờ chỉ xếp tác vụ vào hàng đợi và trả về jobId.

// openai/tools/startDeepAnalysis.ts
import { v4 as uuid } from 'uuid';
import { createJobAndEnqueue } from '../jobs/queue';

// Kiểu giả cho MCP SDK
type StartDeepAnalysisInput = {
  recipientProfile: string;
  budget: number;
};

type StartDeepAnalysisOutput = {
  jobId: string;
  message: string;
};

export async function startDeepAnalysisTool(
  input: StartDeepAnalysisInput
): Promise<StartDeepAnalysisOutput> {
  const jobId = uuid();

  await createJobAndEnqueue({
    id: jobId,
    type: 'deep_gift_analysis',
    status: 'pending',
    payload: {
      recipientProfile: input.recipientProfile,
      budget: input.budget,
    },
    attempts: 0,
    createdAt: new Date(),
    updatedAt: new Date(),
  });

  return {
    jobId,
    message: `Đã khởi chạy phân tích chuyên sâu. ID tác vụ: ${jobId}. Tôi sẽ gửi các cập nhật khi sẵn sàng.`,
  };
}

Điểm mấu chốt:

  • Công cụ MCP chạy nhanh: tối đa vài truy vấn đến CSDL/hàng đợi;
  • nó trả về phản hồi có cấu trúc với jobId, để ChatGPT dùng trong “giải thích với người dùng” và widget GiftGenius có thể lưu trong widgetState.

JSON Schema cho công cụ này chỉ cần mô tả jobId là chuỗi và message là văn bản dễ đọc — mô hình sẽ hiểu đó là định danh tác vụ và có thể tham chiếu đến nó trong các bước tiếp theo của hội thoại.

5. Hàng đợi và worker đơn giản: phiên bản học tập

Để không phải mang theo Redis, RabbitMQ và mọi thứ khác ngay bây giờ, ta làm một hàng đợi in‑memory đơn giản. Trong production thực, tất nhiên sẽ là một dịch vụ riêng (SQS/BullMQ/Cloud Tasks, v.v.), nhưng logic vẫn như vậy.

Trước tiên là phần khung hàng đợi:

// openai/jobs/queue.ts
import type { GiftJob } from './model';

const jobs = new Map<string, GiftJob>();   // "CSDL" trong bộ nhớ
export const queue: string[] = [];         // hàng đợi đơn giản theo id

export async function createJobAndEnqueue(job: GiftJob) {
  jobs.set(job.id, job);
  queue.push(job.id);
}

export function getJob(id: string): GiftJob | undefined {
  return jobs.get(id);
}

export function updateJob(id: string, patch: Partial<GiftJob>) {
  const job = jobs.get(id);
  if (!job) return;
  const updated: GiftJob = { ...job, ...patch, updatedAt: new Date() };
  jobs.set(id, updated);
}

Giờ là worker sơ khai, định kỳ nhìn vào hàng đợi, lấy job và xử lý:

// openai/jobs/worker.ts
import { getJob, updateJob } from './queue';
import { emitJobEvent } from './events';

async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  updateJob(jobId, { status: 'in_progress' });
  await emitJobEvent(jobId, 'job.started', {});

  try {
    // Gọi logic nghiệp vụ tốn thời gian
    const result = await doDeepGiftAnalysis(job.id, job.payload);

    updateJob(jobId, { status: 'completed', result });
    await emitJobEvent(jobId, 'job.completed', { resultSummary: summarize(result) });
  } catch (err) {
    updateJob(jobId, {
      status: 'failed',
      error: (err as Error).message,
    });
    await emitJobEvent(jobId, 'job.failed', { error: 'Internal error' });
  }
}

Và chính worker “vòng lặp” được khởi chạy đâu đó khi app khởi động:

// openai/jobs/workerLoop.ts
import { queue } from './queue';
import { processJob } from './worker';

export function startWorkerLoop() {
  setInterval(async () => {
    const jobId = queue.shift(); // thực tế cần cơ chế chống race condition
    if (!jobId) return;

    await processJob(jobId);
  }, 1000); // kiểm tra hàng đợi mỗi giây
}

Đây là ví dụ học tập. Ở đời thực, thay vì setInterval sẽ là một hàng đợi “đánh thức” worker khi có thông điệp mới. Nhưng ý tưởng chung vẫn rõ: worker tách khỏi công cụ MCP, chạy nền và giao tiếp với MCP server qua sự kiện.

6. Phát sinh sự kiện MCP từ worker

Trong các bài trước bạn đã thấy định dạng sự kiện MCP: type, event_id duy nhất, timestamp, job_idpayload. Giờ cho thấy worker có thể gọi helper emitJobEvent, và helper đó chuyển sự kiện đến ChatGPT qua kênh SSE của MCP server.

Ví dụ helper đơn giản:

// openai/jobs/events.ts
import { randomUUID } from 'crypto';
import { sendMcpEvent } from '../mcp/eventBus';

export async function emitJobEvent(
  jobId: string,
  type: 'job.started' | 'job.progress' | 'job.completed' | 'job.failed',
  payload: unknown
) {
  const event = {
    event_id: randomUUID(),
    type,
    job_id: jobId,
    timestamp: new Date().toISOString(),
    payload,
  };

  await sendMcpEvent(event);
}

Còn sendMcpEvent bên trong MCP server đã biết cách “đẩy” sự kiện này vào SSEServerTransport của MCP SDK: ví dụ thông qua bus sự kiện cục bộ hoặc Redis Pub/Sub, như đã nói trong mô‑đun 12.

Điểm mấu chốt: worker không giao tiếp trực tiếp với ChatGPT. Nó nói chuyện với MCP server, và server đó giữ kết nối SSE rồi chuyển tiếp sự kiện cho client.

7. Tiến độ và kết quả từng phần từ worker

Giờ đến phần thú vị: tiến độ và kết quả từng phần. Trong GiftGenius, phân tích dài có thể chia thành các giai đoạn:

  • thu thập và chuẩn hóa dữ liệu;
  • xây dựng các phân khúc cơ bản;
  • sinh ý tưởng quà tặng sơ bộ;
  • xếp hạng cuối cùng và giải thích bằng văn bản.

Ở mỗi giai đoạn, ta có thể gửi job.progress và đôi khi — job.partial, để UI sớm hiển thị những món quà đầu tiên.

Worker giả định:

async function doDeepGiftAnalysis(jobId: string, payload: GiftJob['payload']) {
  await emitJobEvent(jobId, 'job.progress', { step: 1, totalSteps: 4 });

  const normalized = await collectAndNormalizeData(payload);
  await emitJobEvent(jobId, 'job.progress', { step: 2, totalSteps: 4 });

  const roughGifts = await generateInitialGifts(normalized);
  await emitJobEvent(jobId, 'job.partial', { gifts: roughGifts.slice(0, 3) });

  await emitJobEvent(jobId, 'job.progress', { step: 3, totalSteps: 4 });

  const finalGifts = await rerankAndBeautify(roughGifts);
  await emitJobEvent(jobId, 'job.progress', { step: 4, totalSteps: 4 });

  return finalGifts;
}

Widget khi lắng nghe sự kiện có thể trước tiên hiển thị 3 món quà “nháp” kèm nhãn “Đang tinh chỉnh thêm”, và sau job.completed — cập nhật danh sách và bỏ chỉ báo tải. Tất cả khớp hoàn hảo với các mẫu UX đã nói ở bài 3.

8. Logic retry cho worker

Giờ là phần dễ căng thẳng: lỗi và lặp lại.

Hãy tưởng tượng worker khi xử lý tác vụ phải gọi API danh sách sản phẩm bên ngoài, và API đó thi thoảng trả về 500 hoặc 429. Bỏ tác vụ sau lỗi đầu tiên — không hợp lý. Nhưng cũng không thể chạy lại vô hạn: bạn sẽ tự DDoS mình hoặc dịch vụ bên ngoài.

Ta cần chiến lược retry với độ trễ backoff lũy thừa và giới hạn số lần thử.

Bắt đầu bằng phân loại lỗi, điều này còn hữu ích về sau trong khóa học:

  • tạm thời (transient) — timeout, 500, 503, 429;
  • vĩnh viễn (permanent) — đầu vào sai, tài nguyên không tồn tại;
  • fatal (bug) — lỗi code, TypeError, ngoại lệ bất ngờ.

Chỉ nên lặp lại với lỗi tạm thời. Còn lại cần gắn nhãn 'failed' cho minh bạch.

Đơn giản hóa và viết một helper:

// openai/jobs/retry.ts
export function shouldRetry(error: unknown): boolean {
  if (!(error instanceof Error)) return false;
  // Quy ước: HTTP 5xx hoặc 429
  return /5\d\d|429/.test(error.message);
}

export function getDelayMs(base: number, attempt: number): number {
  const jitter = Math.random() * 100;   // jitter nhỏ
  return base * 2 ** attempt + jitter; // backoff lũy thừa
}

Giờ cập nhật worker để tính đến attempts trong GiftJob:

// openai/jobs/worker.ts
import { getJob, updateJob } from './queue';
import { emitJobEvent } from './events';
import { shouldRetry, getDelayMs } from './retry';

const MAX_ATTEMPTS = 5;

export async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  updateJob(jobId, { status: 'in_progress' });

  try {
    const result = await doDeepGiftAnalysis(job.id, job.payload);

    updateJob(jobId, { status: 'completed', result });
    await emitJobEvent(jobId, 'job.completed', {
      resultSummary: summarize(result),
    });
  } catch (err) {
    const attempts = job.attempts + 1;
    const error = err as Error;

    if (attempts <= MAX_ATTEMPTS && shouldRetry(error)) {
      const delay = getDelayMs(1000, attempts); // 1s,2s,4s...

      updateJob(jobId, { attempts, status: 'pending', error: error.message });

      setTimeout(() => {
        // Trong hàng đợi thực, bạn sẽ enqueue lại job với độ trễ
        processJob(jobId);
      }, delay);

      await emitJobEvent(jobId, 'job.progress', {
        retry: attempts,
        nextAttemptInMs: delay,
      });
    } else {
      updateJob(jobId, { status: 'failed', error: error.message });
      await emitJobEvent(jobId, 'job.failed', {
        error: 'Không thể hoàn tất phân tích sau nhiều lần thử',
      });
    }
  }
}

Có vài điểm đáng chú ý.

Thứ nhất, attempts được lưu trong chính tác vụ — thuận tiện cho logging và observability (trên biểu đồ sẽ thấy đẹp số tác vụ phải retry).

Thứ hai, mỗi lần retry ta gửi job.progress kèm chỉ rõ đây là lần thử số N. Mô hình có thể dùng thông tin này để giải thích cho người dùng rằng “máy chủ quà tặng phản hồi không ổn định, tôi đang thử lại”.

Thứ ba, ta đảm bảo rằng cuối cùng hoặc sẽ gửi job.completed, hoặc job.failed. Không có tác vụ treo “nửa sống nửa chết”.

Hủy ('canceled') — là trạng thái quan trọng khác. Trong ví dụ học tập ta không triển khai, nhưng trong production thường trạng thái này được đặt theo yêu cầu người dùng (nút “Hủy” trong widget) hoặc theo timeout. Khi đó worker ở lần lấy tác vụ tiếp theo sẽ thấy status: 'canceled', không chạy xử lý, và MCP server gửi sự kiện cuối cùng job.canceled.

9. Idempotency và retry: đừng dẫm lên cùng một cái bẫy hai lần

Khi bạn đưa retry vào, rủi ro “làm cùng một việc hai lần” xuất hiện ngay. Trong các mô‑đun commerce điều này rất nghiêm trọng (ví dụ trừ tiền hai lần), nhưng với GiftGenius cũng có kịch bản không hay: gửi hai email giống hệt cho bạn, tạo bản ghi trùng trong phân tích nội bộ, v.v.

Vì vậy hãy bám theo hai nguyên tắc.

Thứ nhất: handler của job phải idempotent.

Nếu bạn gọi nó với cùng jobId nhiều lần (trong khuôn khổ retry hoặc do lỗi), thế giới không được “vỡ”. Để làm vậy:

  • mọi hiệu ứng phụ (ghi CSDL, gửi email, tạo đơn hàng) nên gắn với jobId hoặc một định danh tự nhiên khác, để trong code có thể nhanh chóng kiểm tra xem bước này đã làm chưa;
  • nếu job.status đã là 'completed' hoặc 'failed', lần gọi lại có thể bỏ qua hoặc chỉ trả về kết quả sẵn có.

Ví dụ bảo vệ đơn giản:

export async function processJob(jobId: string) {
  const job = getJob(jobId);
  if (!job) return;

  if (job.status === 'completed' || job.status === 'failed') {
    // Tác vụ đã hoàn tất thành công hoặc thất bại dứt điểm
    return;
  }

  // ... phần code còn lại
}

Thứ hai: sự kiện cũng phải idempotent.

Ta đã nói về event_id và việc client có thể lọc trùng, nhưng phía server cũng nên cẩn trọng: khi worker khởi động lại hoặc khôi phục từ hàng đợi, đừng spam client những job.progress giống nhau nếu không cần thiết.

10. Hàng đợi và worker ở đâu trong kiến trúc của bạn

Trên hình thì đẹp, nhưng worker chạy ở đâu về mặt vật lý? Có vài phương án điển hình.

Worker tích hợp: MCP server và worker — cùng một tiến trình/deploy. Chính nó nhận tool‑call, chính nó chạy worker loop. Ưu điểm: đơn giản, ít dịch vụ, dễ deploy. Nhược điểm — mở rộng: muốn thêm worker thì phải scale cả MCP server.

Worker tách biệt: MCP server — một dịch vụ, worker — dịch vụ khác. Ở giữa là hàng đợi và có thể có Pub/Sub cho sự kiện. Đây là điều thường thấy với BullMQ/Redis và MCP events: MCP server subscribe kênh Redis 'mcp:events', worker publish sự kiện vào đó.

Phương án kết hợp: một instance MCP server chạy cả worker, các instance còn lại — chỉ HTTP/SSE. Hữu ích nếu bạn deploy trên Vercel hoặc nền tảng serverless khác, nơi các tiến trình nền liên tục không mấy “dễ chịu”.

Trong GiftGenius học tập của chúng ta, tạm chọn phương án đầu: MCP server + một worker đơn giản trong tiến trình. Khi sang mô‑đun về production và mở rộng, có thể di chuyển worker thành dịch vụ riêng.

11. Ví dụ: toàn bộ async‑pipeline GiftGenius

Hãy lần lượt xem chuyện gì xảy ra khi người dùng trong chat viết:

“Tôi cần chọn quà phức tạp cho một fan vũ trụ, có tính đến lịch sử mua sắm của anh ấy.”

  1. Mô hình quyết định gọi công cụ start_deep_analysis với tham số hồ sơ người nhận và ngân sách.
  2. Công cụ tạo GiftJob trong CSDL với trạng thái 'pending', đưa vào hàng đợi và trả về jobId + thông điệp xác nhận.
  3. ChatGPT giải thích với người dùng rằng phân tích đã khởi chạy, và có thể chuyển jobId cho widget GiftGenius.
  4. Widget đăng ký lắng nghe sự kiện theo jobId qua SSE, hiển thị thanh tiến độ và trạng thái “Đang thu thập và phân tích dữ liệu”.
  5. Worker, khi thấy job mới trong hàng đợi, cập nhật trạng thái thành 'in_progress' và gửi job.started.
  6. Trong quá trình, nó gửi vài lần job.progress (các giai đoạn) và job.partial (những 23 món quà đầu tiên).
  7. Nếu ở đâu đó API bên ngoài bị lỗi, worker thử lại với backoff lũy thừa, cập nhật attempts và gửi sự kiện kèm thông tin lần thử tiếp theo.
  8. Cuối cùng hoặc nó gửi job.completed với phần tóm tắt và khuyến nghị cuối, hoặc job.failed với giải thích dễ hiểu.
  9. Dựa trên các sự kiện này, widget cập nhật UI, và ChatGPT có thể tạo bản tóm tắt văn bản và đề xuất follow‑up: “Hiển thị thêm ý tưởng”, “Thu hẹp ngân sách”, “Đổi loại quà”.

Về phía người dùng, đây là một quá trình dài nhưng “sống động” và có kiểm soát. Về phía backend — là một async‑pipeline chuẩn với hàng đợi, worker và retry.

12. Bài tập nhỏ (tự luyện)

Nếu muốn củng cố, hãy thử cho GiftGenius:

  • nghĩ ra schema bảng jobs cho CSDL thực: cần index nào, trường nào sẽ dùng để lọc (theo người dùng, theo trạng thái, theo ngày tạo);
  • phác thảo kiểu TypeScript cho HTTP endpoint /api/jobs/:id, để widget trong trường hợp xấu có thể polling trạng thái nếu SSE không khả dụng;
  • mô tả chính sách retry: bao nhiêu lần thử, độ trễ cơ sở, làm gì với các tác vụ vẫn thất bại (bảng dead‑letter đơn giản hay logging + cảnh báo).

Bài này sẽ hữu ích về sau, khi ở các mô‑đun production và observability, ta nói về các metric như “bao nhiêu tác vụ bị kẹt ở trạng thái pending quá N phút”.

13. Các lỗi thường gặp khi làm việc với tác vụ bất đồng bộ

Lỗi #1: làm mọi thứ đồng bộ trong tool‑call.
Cái bẫy phổ biến nhất — cố nhồi mọi công việc nặng vào một MCP tool mà không có hàng đợi. Khi yêu cầu còn ít thì có vẻ chạy được. Nhưng khi tải tăng hoặc API bên ngoài chậm, bạn dính timeout, chat treo và UX cực kỳ tệ. Bất kỳ thao tác nào có thể kéo dài hàng chục giây trở lên nên được thiết kế ngay từ đầu như async‑job với jobId.

Lỗi #2: không có mô hình Job rõ ràng.
Đôi khi lập trình viên cố “chỉ dùng thông điệp trong hàng đợi”, không lưu trạng thái tác vụ trong CSDL. Hệ quả là khó trả lời các câu hỏi cơ bản: “trạng thái tác vụ là gì?”, “ta đã thử chạy bao nhiêu lần?”, “vì sao nó rơi?”.
Mô hình Job rõ ràng với các trường status, attempts, error, createdAt — là nền tảng cho debug, monitoring và UX.

Lỗi #3: không có retry hoặc lặp vô tận.
Có người không hề retry và rơi ngay ở lần 500 đầu, có người viết while (!success) và không giới hạn số lần thử. Trường hợp thứ nhất, bạn mất nhiều tác vụ vì sự cố ngắn hạn; trường hợp thứ hai, bạn tạo “bão” tải và có nguy cơ bị chặn bởi API bên ngoài. Cần điểm cân bằng hợp lý: số lần thử giới hạn + backoff lũy thừa + phân loại lỗi tạm thời/vĩnh viễn.

Lỗi #4: handler không idempotent.
Nếu ở mỗi lần thử bạn, chẳng hạn, tạo bản ghi mới trong hệ thống bên ngoài mà không kiểm tra, thực hiện lại cùng một thanh toán hoặc gửi cùng một email — retry sẽ nhanh chóng thành vấn đề. Handler phải biết tác vụ với jobId này đã hoàn tất trước đó và không lặp lại các hiệu ứng phụ nguy hiểm.

Lỗi #5: không phát sự kiện khi lỗi.
Có trường hợp worker rơi với ngoại lệ bất ngờ, log vào console rồi thôi. Người dùng ngồi mãi chờ job.completed mà không biết mọi thứ đã chết từ lâu. Mọi nhánh xử lý kết thúc bằng lỗi cuối cùng phải dẫn đến job.failed và cập nhật trạng thái Job trong CSDL. Nếu không, luồng MCP của bạn biến thành “hộp đen” một chiều.

Lỗi #6: sự kiện tiến độ quá dày đặc.
Mong muốn “trung thực” và gửi job.progress cho mỗi một phần trăm hoàn thành sẽ làm quá tải mạng, client và MCP server. Tốt hơn gửi tiến độ khi đổi giai đoạn hoặc theo bước lớn (ví dụ mỗi 10%), còn lại chỉ lưu trong log nội bộ.

Lỗi #7: dùng hàng đợi in‑memory trong production.
Ví dụ học tập với queue: string[]Map — tốt để hiểu kiến trúc, nhưng trong hệ thống production thực tế nó sẽ sụp ngay lần restart tiến trình hoặc khi server rơi. Để vận hành nghiêm túc cần hàng đợi và kho lưu trữ bên ngoài: SQS, Pub/Sub, RabbitMQ, Redis Streams, v.v. Phương án in‑memory chỉ phù hợp cho phát triển local và demo đơn giản.

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION