mcpField
D
dm-limits-and-best-practices

dm-limits-and-best-practices

github.com/cognitedata数据与数据库61.9k 次安装1 天前更新
代码仓库

关于 dm-limits-and-best-practices

面向编写或审查 CDF Data Modeling API 代码的开发者参考技能,重点涵盖并发、分页、重试和批处理等运行可靠性实践。

主要功能

  1. 01CDF 并发与资源限制
  2. 02基于信号量的请求调度
  3. 03list 和 query 结果分页
  4. 04upsert 与过滤器批处理
  5. 05搜索与精确过滤的选择
  6. 06死锁和请求限额注意事项

适用场景

  1. 01审查 CDF Data Modeling API 代码
  2. 02处理大量实例结果
  3. 03调度并发 CDF 请求
  4. 04拆分过大的写入和过滤器

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/cognitedata/builder-skills --skill dm-limits-and-best-practices

安装后在 Claude Code 中输入/dm-limits-and-best-practices确认已加载。

详细介绍

CDF 数据建模:限制、并发与最佳实践

这是一份参考技能。在编写或审查调用 CDF 数据建模 API 的代码时,请遵循以下模式。

本技能负责运行时可靠性相关事项:限制、并发、重试、吞吐量和批处理行为。 有关遍历负载的正确性和图特定的故障特征,请参阅 dm-graph-traversal。


DMS 限制参考

有关最新的并发限制、资源限制和属性值限制,请参阅官方文档: https://docs.cognite.com/cdf/dm/dm_reference/dm_limits_and_restrictions

需要注意的要点:

  • 实例的 apply、delete 和 query 操作各自有独立的并发请求限制
  • 超出这些限制会返回 429 Too Many Requests
  • 转换会占用大量并发额度,留给其他客户端的额度因此会减少
  • instances.list 有最大页面大小(请使用分页获取完整结果)
  • instances.query 的表表达式各自有项目数量限制
  • instances.upsert 每次调用最多接受 1000 个项目
  • in 筛选器每个表达式最多接受 1000 个值;更大的集合必须拆分成批次

Search 与 Filter:何时使用哪个

instances.search — 对文本属性进行全文搜索

当需要对字符串字段(名称、描述等)进行模糊/文本匹配时,请使用 instances.search。它支持 operator 参数:

  • AND(默认)— 缩小搜索范围。所有词语都必须匹配。用户提供了具体查询时使用。
  • OR — 范围较广的“广撒网”搜索。任何词语都可以匹配。适用于探索式/输入联想搜索,此时需要最大程度提高召回率。
// Narrow search: find a specific cell by name (AND — all terms must match)
const exactResults = await client.instances.search({
  view: { type: 'view', ...PROCESS_CELL_VIEW },
  query: 'reactor tank A',
  properties: ['name'],
  operator: 'AND',
  limit: 10,
});

// Broad search: typeahead/autocomplete (OR — any term can match)
const broadResults = await client.instances.search({
  view: { type: 'view', ...BATCH_VIEW },
  query: 'BUDE completed',
  properties: ['name', 'description', 'batchStatus'],
  operator: 'OR',
  limit: 10,
});

你可以将 search 与 filter 结合使用,通过精确匹配条件进一步限制结果:

// Text search + exact filter: search for "pump" but only in active nodes
const filtered = await client.instances.search({
  view: { type: 'view', ...PROCESS_CELL_VIEW },
  query: 'pump',
  properties: ['name', 'description'],
  filter: {
    equals: {
      property: getContainerProperty(MY_CONTAINER, 'status'),
      value: 'active',
    },
  },
  limit: 20,
});
使用 filter 的 instances.list / instances.query — 精确匹配筛选

当需要精确、确定性的匹配(相等、范围、in、hasData 等)时,请使用 filter。不进行模糊匹配——值必须完全匹配。

// Exact match: get all completed batches
const completedBatches = await client.instances.list({
  instanceType: 'node',
  sources: [{ source: { type: 'view', ...BATCH_VIEW } }],
  filter: {
    equals: {
      property: getContainerProperty(BATCH_CONTAINER, 'batchStatus'),
      value: 'completed',
    },
  },
  limit: 1000,
});
决策指南
需求使用
用户在搜索框中输入使用 OR 的 instances.search
按名称查找特定项目使用 AND 的 instances.search
按状态、日期范围、枚举值筛选list/query 上的 filter
文本搜索 + 精确约束instances.search + filter
in 筛选器的值数量限制(1000)与批处理

CDF in 筛选器在单个筛选表达式中最多支持 1000 个值。如果需要针对超过 1000 个 ID 进行筛选,请将值拆分成多个块,发出多个请求,然后合并结果。

const IN_FILTER_BATCH_SIZE = 1000;
// Reuse the Chunking Utility defined in the Batching Write Operations section.

async function listByExternalIds(
  client: CogniteClient,
  externalIds: string[],
): Promise<NodeOrEdge[]> {
  const idBatches = chunk(externalIds, IN_FILTER_BATCH_SIZE);
  const responses = await Promise.all(
    idBatches.map((batch) =>
      cdfTaskRunner.schedule(() =>
        client.instances.list({
          instanceType: 'node',
          sources: [{ source: { type: 'view', ...MY_VIEW } }],
          filter: {
            in: {
              property: ['node', 'externalId'],
              values: batch,
            },
          },
          limit: 1000,
        })
      )
    )
  );

  return responses.flatMap((r) => r.items);
}

QueuedTaskRunner(信号量)

始终使用全局 cdfTaskRunner 包装 CDF API 调用。它会限制并发请求,防止出现 429 错误和死锁。

源代码

如果项目尚无信号量工具,请使用以下实现创建 src/shared/utils/semaphore.ts:

/**
 * AbortError thrown when a queued task is cancelled
 */
export class AbortError extends Error {
  public constructor(message: string = 'Aborted') {
    super(message);
    this.name = 'AbortError';
  }
}

type PendingTask<AsyncFn, AsyncFnResult> = {
  resolve: (result: AsyncFnResult) => void;
  reject: (error: unknown) => void;
  fn: AsyncFn;
  key?: string;
};

const DEFAULT_MAX_CONCURRENT_TASKS = 15;

/**
 * QueuedTaskRunner for controlling concurrent operations
 * Used to limit concurrent CDF API requests to avoid rate limiting and deadlocks
 * Essentially a semaphore that allows a limited number of tasks to run at once.
 */
export default class QueuedTaskRunner<
  AsyncFn extends () => Promise<AsyncFnResult>,
  AsyncFnResult = Awaited<ReturnType<AsyncFn>>,
> {
  private pendingTasks: PendingTask<AsyncFn, AsyncFnResult>[] = [];
  private currentPendingTasks: number = 0;
  private readonly maxConcurrentTasks: number = 1;

  public constructor(
    maxConcurrentTasks: number = DEFAULT_MAX_CONCURRENT_TASKS
  ) {
    this.maxConcurrentTasks = maxConcurrentTasks;
  }

  public schedule(
    fn: AsyncFn,
    options: { key?: string } = {}
  ): Promise<AsyncFnResult> {
    this.startTrackingTime();

    return new Promise((resolve, reject) => {
      if (options.key !== undefined) {
        // Cancel existing tasks with the same key (deduplication)
        this.pendingTasks
          .filter((task) => task.key === options.key)
          .forEach((task) => task.reject(new AbortError()));

        this.pendingTasks = this.pendingTasks.filter(
          (task) => task.key !== options.key
        );
      }

      this.pendingTasks.push({
        resolve,
        reject,
        fn,
        key: options.key,
      });

      this.attemptConsumingNextTask();
    });
  }

  public async attemptConsumingNextTask(): Promise<void> {
    if (this.pendingTasks.length === 0) return;
    if (this.currentPendingTasks >= this.maxConcurrentTasks) return;

    const pendingTask = this.pendingTasks.shift();
    if (pendingTask === undefined) {
      throw new Error('pendingTask is undefined, this should never happen');
    }

    this.currentPendingTasks++;
    const { fn, resolve, reject } = pendingTask;

    try {
      const result = await fn();
      resolve(result);
    } catch (e) {
      reject(e);
    } finally {
      this.currentPendingTasks--;
      this.tick();
      this.attemptConsumingNextTask();
    }
  }

  public clearQueue = (): void => {
    this.pendingTasks = [];
  };

  private startTime: number | null = null;

  private startTrackingTime = (): void => {
    if (this.startTime === null) {
      this.startTime = performance.now();
    }
  };

  private tick = (): void => {
    if (this.pendingTasks.length === 0) {
      this.startTime = null;
    }
  };
}

/**
 * Global task runner for CDF API requests
 * Limits concurrent requests to avoid 429 rate limiting and deadlocks
 */
export const cdfTaskRunner = new QueuedTaskRunner(DEFAULT_MAX_CONCURRENT_TASKS);
使用模式

始终使用 cdfTaskRunner.schedule() 包装 CDF 调用:

import { cdfTaskRunner } from '../../../../shared/utils/semaphore';

// Single query
export async function fetchBatches(client: CogniteClient): Promise<CDFBatch[]> {
  return cdfTaskRunner.schedule(async () => {
    const response = await client.instances.query({
      with: { /* ... */ },
      select: { /* ... */ },
    });
    return response.items?.nodes || [];
  });
}

// Multiple parallel queries (safe — the semaphore limits concurrency)
export async function enrichBatch(
  client: CogniteClient,
  batch: CDFBatch
): Promise<BatchEnrichment> {
  const [currentOp, lastOp, cells, material] = await Promise.all([
    fetchCurrentOperation(client, batch.space, batch.externalId),
    fetchLastCompletedOperation(client, batch.space, batch.externalId),
    fetchProcessCells(client, batch.space, batch.externalId),
    fetchMaterial(client, batch.space, batch.externalId),
  ]);
  return { currentOp, lastOp, cells, material };
}

// Each of the above functions internally uses cdfTaskRunner.schedule(),
// so Promise.all is safe — the semaphore prevents exceeding concurrency limits
使用键进行去重

使用 key 选项,在同一查询再次触发时取消过时请求(例如,用户快速更改筛选条件时):

const result = await cdfTaskRunner.schedule(
  async () => client.instances.query({ /* ... */ }),
  { key: `batch-flow-${batchId}` }
);
// If another call with the same key arrives before this completes,
// the previous pending call is rejected with AbortError

分页

DMS instances.list 最多返回 limit 个项目,并提供用于获取下一页的 nextCursor。 DMS instances.query 使用以表表达式名称为键的 cursors 对象。

instances.list 分页
async function fetchAllNodes(client: CogniteClient): Promise<CDFNodeResponse[]> {
  const allItems: CDFNodeResponse[] = [];
  let cursor: string | undefined = undefined;

  do {
    const response = await client.instances.list({
      instanceType: 'node',
      sources: [{ source: { type: 'view', ...MY_VIEW } }],
      filter: {
        equals: {
          property: getContainerProperty(MY_CONTAINER, 'status'),
          value: 'active',
        },
      },
      limit: 1000,
      cursor,
    });

    allItems.push(...response.items);
    cursor = response.nextCursor;
  } while (cursor);

  return allItems;
}
instances.query 分页

query 端点将 nextCursor 作为 Record<string, string> 返回(每个表表达式对应一个游标)。使用 cursors 参数传递该值:

import { isEmpty } from 'lodash';

async function fetchAllResults(
  client: CogniteClient
): Promise<{ results: CDFResult[]; edges: EdgeDefinition[] }> {
  const QUERY_LIMIT = 10_000;

  const fetchPage = async (
    nextCursors?: Record<string, string>
  ): Promise<{ results: CDFResult[]; edges: EdgeDefinition[] }> => {
    const { items, nextCursor } = await client.instances.query({
      with: {
        results: {
          limit: QUERY_LIMIT,
          nodes: {
            filter: {
              hasData: [{ type: 'view', ...RESULT_VIEW }],
            },
          },
        },
        relatedEdges: {
          limit: QUERY_LIMIT,
          edges: {
            from: 'results' as const,
            maxDistance: 1,
            direction: 'outwards' as const,
            filter: {
              equals: {
                property: ['edge', 'type'],
                value: MY_EDGE_TYPE,
              },
            },
          },
        },
      },
      cursors: nextCursors, // Pass cursors from previous page
      select: {
        results: {
          sources: [
            { source: { type: 'view', ...RESULT_VIEW }, properties: ['*'] },
          ],
        },
        relatedEdges: {},
      },
    });

    const results = (items?.results || []) as CDFResult[];
    const edges = (items?.relatedEdges || []).filter(
      (e) => e.instanceType === 'edge'
    );

    // Recurse if more pages exist
    if (!isEmpty(nextCursor)) {
      const next = await fetchPage(nextCursor);
      return {
        results: [...results, ...next.results],
        edges: [...edges, ...next.edges],
      };
    }

    return { results, edges };
  };

  return fetchPage();
}
分页 + QueuedTaskRunner 组合使用

始终使用信号量包装分页获取,以避免耗尽并发额度:

export async function fetchAllWithPagination(
  client: CogniteClient
): Promise<CDFNodeResponse[]> {
  return cdfTaskRunner.schedule(async () => {
    const allItems: CDFNodeResponse[] = [];
    let cursor: string | undefined = undefined;

    do {
      const response = await client.instances.list({
        instanceType: 'node',
        sources: [{ source: { type: 'view', ...MY_VIEW } }],
        filter: { /* ... */ },
        limit: 1000,
        cursor,
      });

      allItems.push(...response.items);
      cursor = response.nextCursor;

      // Optional: break early if you have enough data
      if (allItems.length >= 500) break;
    } while (cursor);

    return allItems;
  });
}

批量写入操作

插入许多实例时,请分块处理,以避免超过应用并发限制。每次 instances.upsert 调用最多接受 1000 个项目。

分块工具
function chunk<T>(arr: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size));
  }
  return chunks;
}
使用 QueuedTaskRunner 批量插入
const UPSERT_BATCH_SIZE = 1000;

async function batchUpsertNodes(
  client: CogniteClient,
  nodes: NodeOrEdgeCreate[]
): Promise<void> {
  const chunks = chunk(nodes, UPSERT_BATCH_SIZE);

  // Process chunks through the semaphore — safe even with Promise.all
  await Promise.all(
    chunks.map((batch) =>
      cdfTaskRunner.schedule(async () => {
        await client.instances.upsert({
          items: batch,
        });
      })
    )
  );
}
使用 QueuedTaskRunner 批量删除

实例删除的并发限制更严格。请使用单独且限制更严格的任务运行器:

import QueuedTaskRunner from '../../../../shared/utils/semaphore';

// Dedicated runner for deletes (stricter concurrency — check docs for current limit)
const deleteTaskRunner = new QueuedTaskRunner(2);

async function batchDeleteNodes(
  client: CogniteClient,
  nodeIds: { space: string; externalId: string }[]
): Promise<void> {
  const chunks = chunk(nodeIds, 1000);

  for (const batch of chunks) {
    await deleteTaskRunner.schedule(async () => {
      await client.instances.delete(
        batch.map((id) => ({
          instanceType: 'node' as const,
          ...id,
        }))
      );
    });
  }
}

硬性限制 — 对查询结果调用 LLM

不要对 instances.list / query / search 命中结果逐项映射聊天补全请求。优先使用一次 Atlas / EOS 侧边栏轮次(integrate-fusion-agent)。如果确实需要逐项补全:每次用户操作最多 5 次,上限 50 次,按 space:externalId:lastUpdatedTime 缓存,且仅由用户主动发起。


常见陷阱

1. 嵌套信号量调用导致死锁

如果函数 A 持有一个信号量槽位,并调用同样需要槽位的函数 B,那么当所有槽位都被占用时,就可能发生死锁。将信号量放在最外层调用处,或确保内部调用不会使用同一个信号量。

// BAD: Nested semaphore — can deadlock
async function fetchAndEnrich(client: CogniteClient) {
  return cdfTaskRunner.schedule(async () => {
    const batches = await fetchBatches(client); // This also calls cdfTaskRunner.schedule!
    // If all slots are held by fetchAndEnrich callers, fetchBatches will never run
  });
}

// GOOD: Let inner functions own the semaphore
async function fetchAndEnrich(client: CogniteClient) {
  const batches = await fetchBatches(client); // Has its own semaphore call
  const enriched = await Promise.all(
    batches.map((b) => enrichBatch(client, b)) // Each has its own semaphore call
  );
  return enriched;
}
2. 忘记分页

DMS 最多返回 limit 个项目。如果不分页,就会在不提示的情况下丢失数据。务必检查 nextCursor:

// BAD: May miss data
const response = await client.instances.list({ limit: 1000, /* ... */ });
const items = response.items; // Could be incomplete!

// GOOD: Paginate
const allItems = [];
let cursor;
do {
  const response = await client.instances.list({ limit: 1000, cursor, /* ... */ });
  allItems.push(...response.items);
  cursor = response.nextCursor;
} while (cursor);
3. 不受限制地使用 Promise.all,且不经过信号量

发起大量并行 API 调用会立即触发 429 限制:

// BAD: Too many simultaneous requests
await Promise.all(batchIds.map((id) => client.instances.query({ /* ... */ })));

// GOOD: Each call goes through the semaphore
await Promise.all(
  batchIds.map((id) =>
    cdfTaskRunner.schedule(() => client.instances.query({ /* ... */ }))
  )
);
4. 每个表表达式的查询限制

instances.query 中的每个表表达式都有自己的 limit。如果遍历可能返回的项目数超过单个表达式的限制,就必须使用 cursors 参数分页。

5. in 筛选条件过大

每个表达式中的 in 筛选条件最多接受 1000 个值。在单个 in 筛选条件中传入超过 1000 个值,可能会失败;具体取决于端点和版本,也可能导致结果不完整。务必将这些值分块,并分批运行请求。


总结清单

  • 使用 cdfTaskRunner.schedule() 包装所有 CDF API 调用
  • 使用 cursor / nextCursor 为 instances.list 调用分页
  • 当数据可能超过限制时,使用 cursors / nextCursor 为 instances.query 调用分页
  • 将写入操作分块,每次 instances.upsert 调用最多处理 1000 个项目
  • 使用单独且限制更严格的任务运行器处理删除操作
  • 避免嵌套调用 cdfTaskRunner.schedule(),以防止死锁
  • 将 Promise.all 与经过信号量包装的函数配合使用,绝不要与原始 API 调用配合使用
  • 文本匹配使用 instances.search,精确匹配查询使用 filter
  • 将 in 筛选条件的值拆分为每批最多 1000 个,并合并响应
  • 对查询结果调用 LLM 的次数不得超过上限(5 / 最多 50),并进行缓存;否则应确保不存在此类调用
  • 当前限制请参阅 https://docs.cognite.com/cdf/dm/dm_reference/dm_limits_and_restrictions

评论

支持 Markdown