11import {
22 executeTSQL ,
3+ QueryError ,
34 type ClickHouseSettings ,
45 type ExecuteTSQLOptions ,
56 type FieldMappings ,
@@ -11,6 +12,11 @@ import { type z } from "zod";
1112import { prisma } from "~/db.server" ;
1213import { env } from "~/env.server" ;
1314import { clickhouseClient } from "./clickhouseInstance.server" ;
15+ import {
16+ queryConcurrencyLimiter ,
17+ DEFAULT_ORG_CONCURRENCY_LIMIT ,
18+ GLOBAL_CONCURRENCY_LIMIT ,
19+ } from "./queryConcurrencyLimiter.server" ;
1420
1521export type { TableSchema , TSQLQueryResult } ;
1622
@@ -69,6 +75,8 @@ export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
6975 /** User ID (optional, null for API calls) */
7076 userId ?: string | null ;
7177 } ;
78+ /** Custom per-org concurrency limit (overrides default) */
79+ customOrgConcurrencyLimit ?: number ;
7280} ;
7381
7482/**
@@ -78,71 +86,107 @@ export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
7886export async function executeQuery < TOut extends z . ZodSchema > (
7987 options : ExecuteQueryOptions < TOut >
8088) : Promise < TSQLQueryResult < z . output < TOut > > > {
81- const { scope, organizationId, projectId, environmentId, history, ...baseOptions } = options ;
82-
83- // Build tenant IDs based on scope
84- const tenantOptions : {
85- organizationId : string ;
86- projectId ?: string ;
87- environmentId ?: string ;
88- } = {
89+ const {
90+ scope,
8991 organizationId,
90- } ;
91-
92- if ( scope === "project" || scope === "environment" ) {
93- tenantOptions . projectId = projectId ;
94- }
95-
96- if ( scope === "environment" ) {
97- tenantOptions . environmentId = environmentId ;
98- }
99-
100- // Build field mappings for project_ref → project_id and environment_id → slug translation
101- const projects = await prisma . project . findMany ( {
102- where : { organizationId } ,
103- select : { id : true , externalRef : true } ,
92+ projectId,
93+ environmentId,
94+ history,
95+ customOrgConcurrencyLimit,
96+ ...baseOptions
97+ } = options ;
98+
99+ // Generate unique request ID for concurrency tracking
100+ const requestId = crypto . randomUUID ( ) ;
101+ const orgLimit = customOrgConcurrencyLimit ?? DEFAULT_ORG_CONCURRENCY_LIMIT ;
102+
103+ // Acquire concurrency slot
104+ const acquireResult = await queryConcurrencyLimiter . acquire ( {
105+ key : organizationId ,
106+ requestId,
107+ keyLimit : orgLimit ,
108+ globalLimit : GLOBAL_CONCURRENCY_LIMIT ,
104109 } ) ;
105110
106- const environments = await prisma . runtimeEnvironment . findMany ( {
107- where : { project : { organizationId } } ,
108- select : { id : true , slug : true } ,
109- } ) ;
111+ if ( ! acquireResult . success ) {
112+ const errorMessage =
113+ acquireResult . reason === "key_limit"
114+ ? `You've exceeded your query concurrency of ${ orgLimit } for this organization. Please try again later.`
115+ : "We're experiencing a lot of queries at the moment. Please try again later." ;
116+ return [ new QueryError ( errorMessage , { query : options . query } ) , null ] ;
117+ }
110118
111- const fieldMappings : FieldMappings = {
112- project : Object . fromEntries ( projects . map ( ( p ) => [ p . id , p . externalRef ] ) ) ,
113- environment : Object . fromEntries ( environments . map ( ( e ) => [ e . id , e . slug ] ) ) ,
114- } ;
119+ try {
120+ // Build tenant IDs based on scope
121+ const tenantOptions : {
122+ organizationId : string ;
123+ projectId ?: string ;
124+ environmentId ?: string ;
125+ } = {
126+ organizationId,
127+ } ;
128+
129+ if ( scope === "project" || scope === "environment" ) {
130+ tenantOptions . projectId = projectId ;
131+ }
132+
133+ if ( scope === "environment" ) {
134+ tenantOptions . environmentId = environmentId ;
135+ }
136+
137+ // Build field mappings for project_ref → project_id and environment_id → slug translation
138+ const projects = await prisma . project . findMany ( {
139+ where : { organizationId } ,
140+ select : { id : true , externalRef : true } ,
141+ } ) ;
115142
116- const result = await executeTSQL ( clickhouseClient . reader , {
117- ...baseOptions ,
118- ...tenantOptions ,
119- fieldMappings,
120- clickhouseSettings : {
121- ...getDefaultClickhouseSettings ( ) ,
122- ...baseOptions . clickhouseSettings , // Allow caller overrides if needed
123- } ,
124- } ) ;
143+ const environments = await prisma . runtimeEnvironment . findMany ( {
144+ where : { project : { organizationId } } ,
145+ select : { id : true , slug : true } ,
146+ } ) ;
125147
126- // If query succeeded and history options provided, save to history
127- if ( result [ 0 ] === null && history ) {
128- const stats = result [ 1 ] . stats ;
129- const byteSeconds = parseFloat ( stats . byte_seconds ) || 0 ;
130- const costInCents = byteSeconds * env . CENTS_PER_QUERY_BYTE_SECOND ;
131-
132- await prisma . customerQuery . create ( {
133- data : {
134- query : options . query ,
135- scope : scopeToEnum [ scope ] ,
136- stats : { ...stats } ,
137- costInCents,
138- source : history . source ,
139- organizationId,
140- projectId : scope === "project" || scope === "environment" ? projectId : null ,
141- environmentId : scope === "environment" ? environmentId : null ,
142- userId : history . userId ?? null ,
148+ const fieldMappings : FieldMappings = {
149+ project : Object . fromEntries ( projects . map ( ( p ) => [ p . id , p . externalRef ] ) ) ,
150+ environment : Object . fromEntries ( environments . map ( ( e ) => [ e . id , e . slug ] ) ) ,
151+ } ;
152+
153+ const result = await executeTSQL ( clickhouseClient . reader , {
154+ ...baseOptions ,
155+ ...tenantOptions ,
156+ fieldMappings,
157+ clickhouseSettings : {
158+ ...getDefaultClickhouseSettings ( ) ,
159+ ...baseOptions . clickhouseSettings , // Allow caller overrides if needed
143160 } ,
144161 } ) ;
145- }
146162
147- return result ;
163+ // If query succeeded and history options provided, save to history
164+ if ( result [ 0 ] === null && history ) {
165+ const stats = result [ 1 ] . stats ;
166+ const byteSeconds = parseFloat ( stats . byte_seconds ) || 0 ;
167+ const costInCents = byteSeconds * env . CENTS_PER_QUERY_BYTE_SECOND ;
168+
169+ await prisma . customerQuery . create ( {
170+ data : {
171+ query : options . query ,
172+ scope : scopeToEnum [ scope ] ,
173+ stats : { ...stats } ,
174+ costInCents,
175+ source : history . source ,
176+ organizationId,
177+ projectId : scope === "project" || scope === "environment" ? projectId : null ,
178+ environmentId : scope === "environment" ? environmentId : null ,
179+ userId : history . userId ?? null ,
180+ } ,
181+ } ) ;
182+ }
183+
184+ return result ;
185+ } finally {
186+ // Always release the concurrency slot
187+ await queryConcurrencyLimiter . release ( {
188+ key : organizationId ,
189+ requestId,
190+ } ) ;
191+ }
148192}
0 commit comments