Merged PR 711: Repositoryロック対応

## 概要
[Task3523: Repositoryロック対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3523)

LicensesRepository
  - allocateLicense
割当先ユーザ取得しロックする処理を追加

WorkflowRepository
 - createtWorkflows
authorの存在確認時にロックを追加
 - updatetWorkflows
authorの存在確認時にロックを追加
UserGroup&UserGroupMemberのロックを追加

AccountsRepository
- updateAccountInfo
プライマリ/セカンダリ管理者ユーザーの存在チェックのロック
(行ロック横展開1で修正されていた)

TasksRepository
- create
タスクの所有者の存在確認とロックを追加
- checkout
対象ユーザの存在確認とロックを追加
- changeCheckoutPermission
対象ユーザの存在確認とロックを追加

UserGroupsRepository
- createTypistGroup
対象ユーザ達のロックを追加
- updateTypistGroup
対象ユーザ達のロックを追加
## レビューポイント
ラフスケッチの、

```
競合ケース E-3. Typistが削除条件判定を行った直後に、チェックアウト候補に削除ユーザーが含まれるTypistGroupが割り当てられる
TypistGroupに割り当たっている時点で削除条件を満たさないので、このケースはないはず
```

ここは未対応でよい認識か。

## 動作確認状況
- ローカルで確認

## 補足
- 相談、参考資料などがあれば
This commit is contained in:
maruyama.t 2024-02-01 06:06:10 +00:00
parent 6d6eee91e0
commit e877942175
4 changed files with 82 additions and 30 deletions

View File

@ -39,6 +39,8 @@ import {
updateEntity, updateEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { User } from '../users/entity/user.entity';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class LicensesRepositoryService { export class LicensesRepositoryService {
@ -559,6 +561,19 @@ export class LicensesRepositoryService {
accountId: number, accountId: number,
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { await this.dataSource.transaction(async (entityManager) => {
// 対象ユーザの存在チェック
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: {
id: userId,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!user) {
throw new UserNotFoundError(`User not exist. userId: ${userId}`);
}
const licenseRepo = entityManager.getRepository(License); const licenseRepo = entityManager.getRepository(License);
const licenseAllocationHistoryRepo = entityManager.getRepository( const licenseAllocationHistoryRepo = entityManager.getRepository(
LicenseAllocationHistory, LicenseAllocationHistory,

View File

@ -48,6 +48,7 @@ import {
deleteEntity, deleteEntity,
} from '../../common/repository'; } from '../../common/repository';
import { Context } from '../../common/log'; import { Context } from '../../common/log';
import { UserNotFoundError } from '../users/errors/types';
@Injectable() @Injectable()
export class TasksRepositoryService { export class TasksRepositoryService {
@ -167,6 +168,20 @@ export class TasksRepositoryService {
permittedSourceStatus: TaskStatus[], permittedSourceStatus: TaskStatus[],
): Promise<void> { ): Promise<void> {
await this.dataSource.transaction(async (entityManager) => { await this.dataSource.transaction(async (entityManager) => {
// 対象ユーザの存在確認
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: {
id: user_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!user) {
throw new TypistUserNotFoundError(
`Typist user not exists. user_id:${user_id}`,
);
}
const taskRepo = entityManager.getRepository(Task); const taskRepo = entityManager.getRepository(Task);
// 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得 // 指定した音声ファイルIDに紐づくTaskの中でStatusが[Uploaded,Inprogress,Pending]であるものを取得
const task = await taskRepo.findOne({ const task = await taskRepo.findOne({
@ -846,6 +861,22 @@ export class TasksRepositoryService {
const createdEntity = await this.dataSource.transaction( const createdEntity = await this.dataSource.transaction(
async (entityManager) => { async (entityManager) => {
// タスクの所有者の存在確認
const userRepo = entityManager.getRepository(User);
const user = await userRepo.findOne({
where: {
id: owner_user_id,
account_id: account_id,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!user) {
throw new UserNotFoundError(
`User not exists. owner_user_id:${owner_user_id}`,
);
}
const audioFileRepo = entityManager.getRepository(AudioFile); const audioFileRepo = entityManager.getRepository(AudioFile);
const newAudioFile = audioFileRepo.create(audioFile); const newAudioFile = audioFileRepo.create(audioFile);
const savedAudioFile = await insertEntity( const savedAudioFile = await insertEntity(
@ -967,6 +998,7 @@ export class TasksRepositoryService {
deleted_at: IsNull(), deleted_at: IsNull(),
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
// idはユニークであるため取得件数の一致でユーザーの存在を確認 // idはユニークであるため取得件数の一致でユーザーの存在を確認
if (typistUserIds.length !== userRecords.length) { if (typistUserIds.length !== userRecords.length) {

View File

@ -123,6 +123,7 @@ export class UserGroupsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (userRecords.length !== typistIds.length) { if (userRecords.length !== typistIds.length) {
throw new TypistIdInvalidError( throw new TypistIdInvalidError(
@ -189,6 +190,7 @@ export class UserGroupsRepositoryService {
email_verified: true, email_verified: true,
}, },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (userRecords.length !== typistIds.length) { if (userRecords.length !== typistIds.length) {
throw new TypistIdInvalidError( throw new TypistIdInvalidError(

View File

@ -87,6 +87,7 @@ export class WorkflowsRepositoryService {
const author = await userRepo.findOne({ const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true }, where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (!author) { if (!author) {
throw new UserNotFoundError( throw new UserNotFoundError(
@ -227,6 +228,37 @@ export class WorkflowsRepositoryService {
): Promise<void> { ): Promise<void> {
return await this.dataSource.transaction(async (entityManager) => { return await this.dataSource.transaction(async (entityManager) => {
const workflowRepo = entityManager.getRepository(Workflow); const workflowRepo = entityManager.getRepository(Workflow);
// authorの存在確認
const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (!author) {
throw new UserNotFoundError(
`author not found or email not verified. id: ${authorId}`,
);
}
// ルーティング候補ユーザーの存在確認
const typistIds = typists.flatMap((typist) =>
typist.typistId ? [typist.typistId] : [],
);
const typistUsers = await userRepo.find({
where: {
account_id: accountId,
id: In(typistIds),
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
});
if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError(
`typist not found or email not verified. ids: ${typistIds}`,
);
}
// ワークフローの存在確認 // ワークフローの存在確認
const targetWorkflow = await workflowRepo.findOne({ const targetWorkflow = await workflowRepo.findOne({
@ -239,18 +271,6 @@ export class WorkflowsRepositoryService {
); );
} }
// authorの存在確認
const userRepo = entityManager.getRepository(User);
const author = await userRepo.findOne({
where: { account_id: accountId, id: authorId, email_verified: true },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
if (!author) {
throw new UserNotFoundError(
`author not found or email not verified. id: ${authorId}`,
);
}
// worktypeの存在確認 // worktypeの存在確認
if (worktypeId !== undefined) { if (worktypeId !== undefined) {
const worktypeRepo = entityManager.getRepository(Worktype); const worktypeRepo = entityManager.getRepository(Worktype);
@ -279,24 +299,6 @@ export class WorkflowsRepositoryService {
} }
} }
// ルーティング候補ユーザーの存在確認
const typistIds = typists.flatMap((typist) =>
typist.typistId ? [typist.typistId] : [],
);
const typistUsers = await userRepo.find({
where: {
account_id: accountId,
id: In(typistIds),
email_verified: true,
},
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
});
if (typistUsers.length !== typistIds.length) {
throw new UserNotFoundError(
`typist not found or email not verified. ids: ${typistIds}`,
);
}
// ルーティング候補ユーザーグループの存在確認 // ルーティング候補ユーザーグループの存在確認
const groupIds = typists.flatMap((typist) => { const groupIds = typists.flatMap((typist) => {
return typist.typistGroupId ? [typist.typistGroupId] : []; return typist.typistGroupId ? [typist.typistGroupId] : [];
@ -305,6 +307,7 @@ export class WorkflowsRepositoryService {
const typistGroups = await userGroupRepo.find({ const typistGroups = await userGroupRepo.find({
where: { account_id: accountId, id: In(groupIds) }, where: { account_id: accountId, id: In(groupIds) },
comment: `${context.getTrackingId()}_${new Date().toUTCString()}`, comment: `${context.getTrackingId()}_${new Date().toUTCString()}`,
lock: { mode: 'pessimistic_write' },
}); });
if (typistGroups.length !== groupIds.length) { if (typistGroups.length !== groupIds.length) {
throw new TypistGroupNotExistError( throw new TypistGroupNotExistError(