Skip to content

Commit a9a40c0

Browse files
authored
Merge pull request #22 from cnbrown04/role-creation
fix: memory issues
2 parents db84000 + 134f370 commit a9a40c0

File tree

3 files changed

+155
-45
lines changed

3 files changed

+155
-45
lines changed

src/abac/adapter.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ interface SqliteConfig {
3232
type: "sqlite";
3333
uri: string; // Path to the SQLite database file
3434
filename?: string; // Optional, overrides uri for file path
35+
readonly?: boolean; // Open database in read-only mode
3536
}
3637
interface AbacAdapterConfig {
3738
db: MysqlConfig | PostgresConfig | SqliteConfig;
@@ -64,9 +65,13 @@ async function createDialect(
6465
return new MysqlDialect({
6566
pool: createPool({
6667
uri: config.uri,
67-
connectionLimit: config.connectionLimit ?? 10,
68+
connectionLimit: config.connectionLimit ?? 5,
6869
waitForConnections: config.waitForConnections ?? true,
6970
queueLimit: config.queueLimit ?? 0,
71+
acquireTimeout: 60000,
72+
timeout: 60000,
73+
idleTimeout: 300000,
74+
maxIdle: 2,
7075
}),
7176
});
7277
} catch (error) {
@@ -96,7 +101,7 @@ async function createDialect(
96101
return new PostgresDialect({
97102
pool: new Pool({
98103
connectionString: config.uri,
99-
max: config.connectionLimit ?? 10,
104+
max: config.connectionLimit ?? 5,
100105
ssl: config.ssl ?? false,
101106
}),
102107
});
@@ -135,7 +140,18 @@ async function createDialect(
135140

136141
// Use filename if provided, otherwise use uri
137142
const dbPath = config.filename || config.uri;
138-
const db = new Database(dbPath);
143+
const db = new Database(dbPath, {
144+
readonly: config.readonly ?? false,
145+
fileMustExist: false,
146+
timeout: 5000,
147+
});
148+
149+
// Configure SQLite for better memory management
150+
db.pragma("journal_mode = WAL");
151+
db.pragma("synchronous = NORMAL");
152+
db.pragma("cache_size = -8000"); // 8MB cache
153+
db.pragma("temp_store = MEMORY");
154+
db.pragma("mmap_size = 268435456"); // 256MB mmap
139155

140156
return new SqliteDialect({
141157
database: db,

src/abac/funcs.ts

Lines changed: 114 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ export interface PolicyWithRules {
5454
targets: any[];
5555
}
5656

57+
// Attribute cache for reducing database queries
58+
const attributeCache = new Map<string, { attributes: Map<string, AttributeValue>; timestamp: number }>();
59+
const CACHE_TTL = 300000; // 5 minutes
60+
5761
// Debug logging utility
5862
function debugLog(
5963
config: AuthorizationConfig | undefined,
@@ -65,6 +69,16 @@ function debugLog(
6569
}
6670
}
6771

72+
// Cache cleanup to prevent memory growth
73+
function cleanupCache() {
74+
const now = Date.now();
75+
for (const [key, value] of attributeCache) {
76+
if (now - value.timestamp > CACHE_TTL) {
77+
attributeCache.delete(key);
78+
}
79+
}
80+
}
81+
6882
// Operators for rule evaluation
6983
const OPERATORS = {
7084
equals: (a: any, b: any) => a === b,
@@ -167,13 +181,27 @@ export async function canUserPerformAction(
167181
}
168182

169183
/**
170-
* Gather all relevant attributes for the authorization request
184+
* Gather all relevant attributes for the authorization request with caching
171185
*/
172186
async function gatherAttributes(
173187
db: Kysely<Database>,
174188
request: AuthorizationRequest,
175189
config?: AuthorizationConfig
176190
): Promise<Map<string, AttributeValue>> {
191+
// Clean up old cache entries periodically
192+
if (Math.random() < 0.1) { // 10% chance to clean up on each call
193+
cleanupCache();
194+
}
195+
196+
// Create cache key based on request parameters
197+
const cacheKey = `${request.subjectId}-${request.resourceId || 'no-resource'}-${request.actionName}`;
198+
const cached = attributeCache.get(cacheKey);
199+
200+
if (cached && (Date.now() - cached.timestamp) < CACHE_TTL) {
201+
debugLog(config, "🎯 Using cached attributes for:", cacheKey);
202+
return new Map(cached.attributes);
203+
}
204+
177205
const attributeMap = new Map<string, AttributeValue>();
178206

179207
debugLog(
@@ -407,11 +435,17 @@ async function gatherAttributes(
407435
}
408436
}
409437

438+
// Cache the results
439+
attributeCache.set(cacheKey, {
440+
attributes: new Map(attributeMap),
441+
timestamp: Date.now()
442+
});
443+
410444
return attributeMap;
411445
}
412446

413447
/**
414-
* Add dynamic environment attributes from context
448+
* Add dynamic environment attributes from context - optimized for memory
415449
*/
416450
function addDynamicEnvironmentAttributes(
417451
attributeMap: Map<string, AttributeValue>,
@@ -420,21 +454,22 @@ function addDynamicEnvironmentAttributes(
420454
) {
421455
debugLog(config, "🔄 Adding dynamic environment attributes...");
422456

457+
// Reuse Date object to reduce allocation
458+
const now = new Date();
459+
const currentTimeString = now.toISOString();
460+
423461
// Current time
424462
attributeMap.set("environment.current_time", {
425463
id: "env-current-time",
426464
name: "current_time",
427465
type: "string",
428466
category: "environment",
429-
value: new Date().toISOString(),
467+
value: currentTimeString,
430468
});
431-
debugLog(
432-
config,
433-
` ✓ environment.current_time = "${new Date().toISOString()}"`
434-
);
469+
debugLog(config, ` ✓ environment.current_time = "${currentTimeString}"`);
435470

436471
// Current day of week
437-
const dayOfWeek = new Date().getDay().toString();
472+
const dayOfWeek = now.getDay().toString();
438473
attributeMap.set("environment.day_of_week", {
439474
id: "env-day-of-week",
440475
name: "day_of_week",
@@ -444,20 +479,26 @@ function addDynamicEnvironmentAttributes(
444479
});
445480
debugLog(config, ` ✓ environment.day_of_week = "${dayOfWeek}"`);
446481

447-
// Add context attributes
448-
if (context) {
482+
// Add context attributes - limit context size to prevent memory issues
483+
if (context && Object.keys(context).length > 0) {
449484
debugLog(config, "🎯 Adding context attributes...");
450-
Object.entries(context).forEach(([key, value]) => {
485+
const contextEntries = Object.entries(context).slice(0, 50); // Limit to 50 context attributes
486+
487+
for (const [key, value] of contextEntries) {
451488
const envKey = `environment.${key}`;
452489
attributeMap.set(envKey, {
453490
id: `ctx-${key}`,
454491
name: key,
455492
type: typeof value,
456493
category: "environment",
457-
value: String(value),
494+
value: String(value).slice(0, 1000), // Limit value length to 1000 chars
458495
});
459496
debugLog(config, ` ✓ ${envKey} = "${value}"`);
460-
});
497+
}
498+
499+
if (Object.keys(context).length > 50) {
500+
console.warn(`Context has ${Object.keys(context).length} attributes, limiting to 50 for memory efficiency`);
501+
}
461502
}
462503
}
463504

@@ -931,7 +972,7 @@ function makeFinalDecision(
931972
}
932973

933974
/**
934-
* Log the access request for auditing
975+
* Log the access request for auditing - optimized for memory usage
935976
*/
936977
async function logAccessRequest(
937978
db: Kysely<Database>,
@@ -965,6 +1006,20 @@ async function logAccessRequest(
9651006
return;
9661007
}
9671008

1009+
// Optimize JSON stringification to reduce memory usage
1010+
const appliedPoliciesString = evaluations.length > 0
1011+
? JSON.stringify(evaluations.map(e => ({
1012+
policyId: e.policyId,
1013+
policyName: e.policyName,
1014+
effect: e.effect,
1015+
matches: e.matches,
1016+
})))
1017+
: "[]";
1018+
1019+
const contextString = request.context && Object.keys(request.context).length > 0
1020+
? JSON.stringify(request.context)
1021+
: "{}";
1022+
9681023
await db
9691024
.insertInto("access_request")
9701025
.values({
@@ -973,15 +1028,8 @@ async function logAccessRequest(
9731028
resource_id: resourceId,
9741029
action_id: action?.id || request.actionName,
9751030
decision: decision.decision,
976-
applied_policies: JSON.stringify(
977-
evaluations.map((e) => ({
978-
policyId: e.policyId,
979-
policyName: e.policyName,
980-
effect: e.effect,
981-
matches: e.matches,
982-
}))
983-
),
984-
request_context: JSON.stringify(request.context || {}),
1031+
applied_policies: appliedPoliciesString,
1032+
request_context: contextString,
9851033
processing_time_ms: processingTime,
9861034
created_at: new Date(),
9871035
})
@@ -1060,7 +1108,7 @@ export async function canUserDelete(
10601108
}
10611109

10621110
/**
1063-
* Batch authorization check for multiple resources
1111+
* Batch authorization check for multiple resources with concurrency limiting
10641112
*/
10651113
export async function canUserPerformActionOnResources(
10661114
db: Kysely<Database>,
@@ -1071,27 +1119,51 @@ export async function canUserPerformActionOnResources(
10711119
config?: AuthorizationConfig
10721120
): Promise<Record<string, AuthorizationResult>> {
10731121
const results: Record<string, AuthorizationResult> = {};
1122+
const BATCH_SIZE = 10; // Limit concurrent operations to prevent memory spikes
1123+
1124+
// Process resources in batches to limit memory usage
1125+
for (let i = 0; i < resourceIds.length; i += BATCH_SIZE) {
1126+
const batch = resourceIds.slice(i, i + BATCH_SIZE);
1127+
1128+
const promises = batch.map(async (resourceId) => {
1129+
try {
1130+
const result = await canUserPerformAction(
1131+
db,
1132+
{
1133+
subjectId: userId,
1134+
resourceId,
1135+
actionName,
1136+
context,
1137+
},
1138+
config
1139+
);
1140+
return { resourceId, result, success: true };
1141+
} catch (error) {
1142+
debugLog(config, `Error processing resource ${resourceId}:`, error);
1143+
return {
1144+
resourceId,
1145+
result: {
1146+
decision: "indeterminate" as const,
1147+
reason: `Error processing resource: ${error instanceof Error ? error.message : 'Unknown error'}`,
1148+
appliedPolicies: [],
1149+
processingTimeMs: 0,
1150+
},
1151+
success: false
1152+
};
1153+
}
1154+
});
10741155

1075-
// Process in parallel for better performance
1076-
const promises = resourceIds.map(async (resourceId) => {
1077-
const result = await canUserPerformAction(
1078-
db,
1079-
{
1080-
subjectId: userId,
1081-
resourceId,
1082-
actionName,
1083-
context,
1084-
},
1085-
config
1086-
);
1087-
return { resourceId, result };
1088-
});
1156+
const batchResults = await Promise.all(promises);
10891157

1090-
const resolvedResults = await Promise.all(promises);
1158+
batchResults.forEach(({ resourceId, result }) => {
1159+
results[resourceId] = result;
1160+
});
10911161

1092-
resolvedResults.forEach(({ resourceId, result }) => {
1093-
results[resourceId] = result;
1094-
});
1162+
// Add small delay between batches to prevent overwhelming the database
1163+
if (i + BATCH_SIZE < resourceIds.length) {
1164+
await new Promise(resolve => setTimeout(resolve, 10));
1165+
}
1166+
}
10951167

10961168
return results;
10971169
}

src/abac/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,7 @@ const abac = (db: Kysely<Database>, debugLogs: boolean) => {
538538
return context.path.startsWith("/sign-up");
539539
},
540540
handler: async (ctx) => {
541+
let dbConnection: any = null;
541542
try {
542543
const contextData = ctx as any;
543544

@@ -565,6 +566,16 @@ const abac = (db: Kysely<Database>, debugLogs: boolean) => {
565566
"Unexpected error in sign-up handler:",
566567
unexpectedError
567568
);
569+
570+
// Ensure any database connections are properly cleaned up
571+
if (dbConnection && typeof dbConnection.destroy === 'function') {
572+
try {
573+
await dbConnection.destroy();
574+
} catch (cleanupError) {
575+
console.error("Error cleaning up database connection:", cleanupError);
576+
}
577+
}
578+
568579
return {
569580
message: "Unexpected error occurred",
570581
error: String(unexpectedError),
@@ -577,6 +588,7 @@ const abac = (db: Kysely<Database>, debugLogs: boolean) => {
577588
return context.path.startsWith("/sign-in");
578589
},
579590
handler: async (ctx) => {
591+
let dbConnection: any = null;
580592
try {
581593
const contextData = ctx as any;
582594

@@ -601,6 +613,16 @@ const abac = (db: Kysely<Database>, debugLogs: boolean) => {
601613
"Unexpected error in sign-in handler:",
602614
unexpectedError
603615
);
616+
617+
// Ensure any database connections are properly cleaned up
618+
if (dbConnection && typeof dbConnection.destroy === 'function') {
619+
try {
620+
await dbConnection.destroy();
621+
} catch (cleanupError) {
622+
console.error("Error cleaning up database connection:", cleanupError);
623+
}
624+
}
625+
604626
return ctx; // Continue processing for sign-in
605627
}
606628
},

0 commit comments

Comments
 (0)