refactor(core): Cache workflow ownership (#6738)

* refactor: Set up ownership service

* refactor: Specify cache keys and values

* refactor: Replace util with service calls

* test: Mock service in tests

* refactor: Use dependency injection

* test: Write tests

* refactor: Apply feedback from Omar and Micha

* test: Fix tests

* test: Fix missing spot

* refactor: Return user entity from cache

* refactor: More dependency injection!
This commit is contained in:
Iván Ovejero
2023-07-31 11:37:09 +02:00
committed by GitHub
parent 72523462ea
commit ffae8edce3
13 changed files with 166 additions and 44 deletions

View File

@@ -9,6 +9,10 @@ import { LoggerProxy, jsonStringify } from 'n8n-workflow';
@Service()
export class CacheService {
/**
* Keys and values:
* - `'cache:workflow-owner:${workflowId}'`: `User`
*/
private cache: RedisCache | MemoryCache | undefined;
async init() {

View File

@@ -1,15 +1,18 @@
import { EventEmitter } from 'events';
import { Service } from 'typedi';
import Container, { Service } from 'typedi';
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import { LoggerProxy } from 'n8n-workflow';
import { StatisticsNames } from '@db/entities/WorkflowStatistics';
import { WorkflowStatisticsRepository } from '@db/repositories';
import { getWorkflowOwner } from '@/UserManagement/UserManagementHelper';
import { UserService } from '@/user/user.service';
import { OwnershipService } from './ownership.service';
@Service()
export class EventsService extends EventEmitter {
constructor(private repository: WorkflowStatisticsRepository) {
constructor(
private repository: WorkflowStatisticsRepository,
private ownershipService: OwnershipService,
) {
super({ captureRejections: true });
if ('SKIP_STATISTICS_EVENTS' in process.env) return;
@@ -41,7 +44,7 @@ export class EventsService extends EventEmitter {
const upsertResult = await this.repository.upsertWorkflowStatistics(name, workflowId);
if (name === 'production_success' && upsertResult === 'insert') {
const owner = await getWorkflowOwner(workflowId);
const owner = await Container.get(OwnershipService).getWorkflowOwnerCached(workflowId);
const metrics = {
user_id: owner.id,
workflow_id: workflowId,
@@ -72,7 +75,7 @@ export class EventsService extends EventEmitter {
if (insertResult === 'failed') return;
// Compile the metrics since this was a new data loaded event
const owner = await getWorkflowOwner(workflowId);
const owner = await this.ownershipService.getWorkflowOwnerCached(workflowId);
let metrics = {
user_id: owner.id,

View File

@@ -0,0 +1,36 @@
import { Service } from 'typedi';
import { CacheService } from './cache.service';
import { RoleRepository, SharedWorkflowRepository, UserRepository } from '@/databases/repositories';
import type { User } from '@/databases/entities/User';
@Service()
export class OwnershipService {
constructor(
private cacheService: CacheService,
private userRepository: UserRepository,
private roleRepository: RoleRepository,
private sharedWorkflowRepository: SharedWorkflowRepository,
) {}
/**
* Retrieve the user who owns the workflow. Note that workflow ownership is **immutable**.
*/
async getWorkflowOwnerCached(workflowId: string) {
const cachedValue = await this.cacheService.get<User>(`cache:workflow-owner:${workflowId}`);
if (cachedValue) return this.userRepository.create(cachedValue);
const workflowOwnerRole = await this.roleRepository.findWorkflowOwnerRole();
if (!workflowOwnerRole) throw new Error('Failed to find workflow owner role');
const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({
where: { workflowId, roleId: workflowOwnerRole.id },
relations: ['user', 'user.globalRole'],
});
void this.cacheService.set(`cache:workflow-owner:${workflowId}`, sharedWorkflow.user);
return sharedWorkflow.user;
}
}