From 4399a61f2b6c17049380f090f642c6c3ce036eef Mon Sep 17 00:00:00 2001 From: "oura.a" Date: Wed, 6 Dec 2023 00:46:53 +0000 Subject: [PATCH] =?UTF-8?q?Merged=20PR=20600:=20[=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E3=82=BB=E3=83=B3=E3=82=B9=E3=82=A2=E3=83=A9=E3=83=BC=E3=83=88?= =?UTF-8?q?=E6=94=B9=E5=96=84]=E3=83=AA=E3=83=88=E3=83=A9=E3=82=A4?= =?UTF-8?q?=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 概要 [Task3025: [ライセンスアラート改善]リトライ対応](https://paruru.nds-tyo.co.jp:8443/tfs/ReciproCollection/fa4924a4-d079-4fab-9fb5-a9a11eb205f0/_workitems/edit/3025) ライセンスアラート処理にリトライ処理を追加しました。 メールの多重送信を防ぐために、送信成功したメールについてはredisに保存し、送信時にキャッシュをチェックする処理を入れました。 ## レビューポイント 処理の流れが妥当か。 redisに保存するキー、値は適切か。 if文のネストが相当深くなってしまったが、改善できるポイントはあるか。 ## UIの変更 なし ## 動作確認状況 ローカルで動作確認済み。(テスト用コードで無理やりエラーを発生させての確認) ## 補足 なし --- dictation_function/host.json | 7 +- dictation_function/src/adb2c/adb2c.ts | 35 +- .../src/common/cache/constants.ts | 6 +- dictation_function/src/common/cache/index.ts | 25 +- .../src/functions/licenseAlert.ts | 505 ++++++++++++++---- .../src/functions/redisTimerTest.ts | 29 - dictation_function/src/redis/redis.ts | 22 + .../src/test/licenseAlert.spec.ts | 43 +- 8 files changed, 500 insertions(+), 172 deletions(-) delete mode 100644 dictation_function/src/functions/redisTimerTest.ts diff --git a/dictation_function/host.json b/dictation_function/host.json index 9df9136..a75de3b 100644 --- a/dictation_function/host.json +++ b/dictation_function/host.json @@ -11,5 +11,10 @@ "extensionBundle": { "id": "Microsoft.Azure.Functions.ExtensionBundle", "version": "[4.*, 5.0.0)" + }, + "retry": { + "strategy": "fixedDelay", + "maxRetryCount": 3, + "delayInterval": "00:00:10" } -} \ No newline at end of file +} diff --git a/dictation_function/src/adb2c/adb2c.ts b/dictation_function/src/adb2c/adb2c.ts index f58f9b3..b9bde0c 100644 --- a/dictation_function/src/adb2c/adb2c.ts +++ b/dictation_function/src/adb2c/adb2c.ts @@ -5,8 +5,8 @@ import { AdB2cResponse, AdB2cUser } from "./types/types"; import { error } from "console"; import { makeADB2CKey, restoreAdB2cID } from "../common/cache"; import { promisify } from "util"; -import { createRedisClient } from "../redis/redis"; import { InvocationContext } from "@azure/functions"; +import { RedisClient } from "redis"; export class Adb2cTooManyRequestsError extends Error {} @@ -23,16 +23,24 @@ export class AdB2cService { ) { throw error; } - const credential = new ClientSecretCredential( - process.env.ADB2C_TENANT_ID, - process.env.ADB2C_CLIENT_ID, - process.env.ADB2C_CLIENT_SECRET - ); - const authProvider = new TokenCredentialAuthenticationProvider(credential, { - scopes: ["https://graph.microsoft.com/.default"], - }); + try { + const credential = new ClientSecretCredential( + process.env.ADB2C_TENANT_ID, + process.env.ADB2C_CLIENT_ID, + process.env.ADB2C_CLIENT_SECRET + ); + const authProvider = new TokenCredentialAuthenticationProvider( + credential, + { + scopes: ["https://graph.microsoft.com/.default"], + } + ); - this.graphClient = Client.initWithMiddleware({ authProvider }); + this.graphClient = Client.initWithMiddleware({ authProvider }); + } catch (error) { + console.log(error); + throw error; + } } /** @@ -42,9 +50,9 @@ export class AdB2cService { */ async getUsers( context: InvocationContext, + redisClient: RedisClient, externalIds: string[] - ): Promise { - const redisClient = createRedisClient(); + ): Promise { try { const b2cUsers: AdB2cUser[] = []; const keys = externalIds.map((externalId) => makeADB2CKey(externalId)); @@ -123,7 +131,7 @@ export class AdB2cService { return [...cachedUsers, ...b2cUsers]; } else { - return undefined; + return b2cUsers; } } catch (e) { const { statusCode } = e; @@ -132,7 +140,6 @@ export class AdB2cService { } throw e; } finally { - redisClient.quit; } } } diff --git a/dictation_function/src/common/cache/constants.ts b/dictation_function/src/common/cache/constants.ts index da6c13e..ffa7fe7 100644 --- a/dictation_function/src/common/cache/constants.ts +++ b/dictation_function/src/common/cache/constants.ts @@ -1 +1,5 @@ -export const ADB2C_PREFIX = "adb2c-external-id:" \ No newline at end of file +export const ADB2C_PREFIX = "adb2c-external-id:"; +export const SEND_COMPLETE_PREFIX = "send-complete-id:"; +export const MAIL_U103 = "[U103]"; +export const MAIL_U104 = "[U104]"; +export const DONE = "Done"; // メール送信成功時にredisにキャッシュする値 diff --git a/dictation_function/src/common/cache/index.ts b/dictation_function/src/common/cache/index.ts index 067be25..27c0eab 100644 --- a/dictation_function/src/common/cache/index.ts +++ b/dictation_function/src/common/cache/index.ts @@ -1,4 +1,4 @@ -import { ADB2C_PREFIX } from './constants'; +import { ADB2C_PREFIX, SEND_COMPLETE_PREFIX } from "./constants"; /** * ADB2Cのユーザー格納用のキーを生成する @@ -6,8 +6,8 @@ import { ADB2C_PREFIX } from './constants'; * @returns キャッシュのキー */ export const makeADB2CKey = (externalId: string): string => { - return `${ADB2C_PREFIX}${externalId}`; -} + return `${ADB2C_PREFIX}${externalId}`; +}; /** * ADB2Cのユーザー格納用のキーから外部ユーザーIDを取得する @@ -15,5 +15,20 @@ export const makeADB2CKey = (externalId: string): string => { * @returns 外部ユーザーID */ export const restoreAdB2cID = (key: string): string => { - return key.replace(ADB2C_PREFIX, ''); -} \ No newline at end of file + return key.replace(ADB2C_PREFIX, ""); +}; + +/** + * ライセンスアラートメール送信履歴格納用のキーを生成する + * @param formattedDate 当日の日付(YYYY:MM:DD) + * @param externalId 外部ユーザーID + * @param mail メール種別 + * @returns キャッシュのキー + */ +export const makeSendCompKey = ( + formattedDate: string, + externalId: string, + mail: string +): string => { + return `${SEND_COMPLETE_PREFIX}${formattedDate}${mail}${externalId}`; +}; diff --git a/dictation_function/src/functions/licenseAlert.ts b/dictation_function/src/functions/licenseAlert.ts index f8f2a71..bbab8b0 100644 --- a/dictation_function/src/functions/licenseAlert.ts +++ b/dictation_function/src/functions/licenseAlert.ts @@ -19,36 +19,166 @@ import { createMailContentOfLicenseExpiringSoon } from "../sendgrid/mailContents import { AdB2cService } from "../adb2c/adb2c"; import { SendGridService } from "../sendgrid/sendgrid"; import { getMailFrom } from "../common/getEnv/getEnv"; +import { createRedisClient } from "../redis/redis"; +import { RedisClient } from "redis"; +import { promisify } from "util"; +import { makeSendCompKey } from "../common/cache"; +import { + MAIL_U103, + MAIL_U104, + SEND_COMPLETE_PREFIX, + DONE, +} from "../common/cache/constants"; export async function licenseAlertProcessing( context: InvocationContext, datasource: DataSource, + redisClient: RedisClient, sendgrid: SendGridService, adb2c: AdB2cService ) { - context.log("[IN]licenseAlertProcessing"); - const mailFrom = getMailFrom(); - const accountRepository = datasource.getRepository(Account); + try { + context.log("[IN]licenseAlertProcessing"); - // 第五のアカウントを取得 - const accounts = await accountRepository.find({ - where: { - tier: TIERS.TIER5, - }, - relations: { - primaryAdminUser: true, - secondaryAdminUser: true, - }, - }); + // redisのキー用 + const currentDate = new DateWithZeroTime(); + const formattedDate = `${currentDate.getFullYear()}-${( + currentDate.getMonth() + 1 + ).toString()}-${currentDate.getDate().toString()}`; + const keysAsync = promisify(redisClient.keys).bind(redisClient); - const licenseRepository = datasource.getRepository(License); - const currentDate = new DateWithZeroTime(); - const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); - const currentDateWithZeroTime = new DateWithZeroTime(); - const currentDateWithDayEndTime = new DateWithDayEndTime(); - const sendTargetAccounts = [] as accountInfo[]; + // メール送信対象のアカウント情報を取得 + const sendTargetAccounts = await getAlertMailTargetAccount( + context, + datasource + ); - const counts = async () => { + // adb2cからメールアドレスを取得し、上記で取得したアカウントにマージする + const sendTargetAccountsMargedAdb2c = await createAccountInfo( + context, + redisClient, + adb2c, + sendTargetAccounts + ); + + // メール送信 + await sendAlertMail( + context, + redisClient, + sendgrid, + sendTargetAccountsMargedAdb2c, + formattedDate + ); + + // 最後まで処理が正常に通ったら、redisに保存した送信情報を削除する + try { + const delAsync = promisify(redisClient.del).bind(redisClient); + + const keys = await keysAsync(`${SEND_COMPLETE_PREFIX}${formattedDate}*`); + console.log(`delete terget:${keys}`); + if (keys.length > 0) { + const delResult = await delAsync(...keys); + console.log(`delete number:${delResult}`); + } + } catch (e) { + context.log("redis delete failed"); + throw e; + } + } catch (e) { + throw e; + } finally { + context.log("[OUT]licenseAlertProcessing"); + } +} + +export async function licenseAlert( + myTimer: Timer, + context: InvocationContext +): Promise { + context.log("[IN]licenseAlert"); + + dotenv.config({ path: ".env" }); + dotenv.config({ path: ".env.local", override: true }); + let datasource: DataSource; + try { + datasource = new DataSource({ + type: "mysql", + host: process.env.DB_HOST, + port: Number(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: [User, Account, License], + }); + await datasource.initialize(); + } catch (e) { + context.log("database initialize failed."); + context.error(e); + throw e; + } + + let redisClient: RedisClient; + try { + // redis接続 + redisClient = createRedisClient(); + } catch (e) { + context.log("redis client create failed."); + context.error(e); + throw e; + } + + try { + const adb2c = new AdB2cService(); + const sendgrid = new SendGridService(); + await licenseAlertProcessing( + context, + datasource, + redisClient, + sendgrid, + adb2c + ); + } catch (e) { + context.log("licenseAlertProcessing failed."); + context.error(e); + throw e; + } finally { + await datasource.destroy(); + redisClient.quit; + context.log("[OUT]licenseAlert"); + } +} + +/** + * アラートメールを送信する対象のアカウントを取得する + * @param context + * @param datasource + * @returns accountInfo[] メール送信対象のアカウント情報 + */ +async function getAlertMailTargetAccount( + context: InvocationContext, + datasource: DataSource +): Promise { + try { + context.log("[IN]getAlertMailTargetAccount"); + const currentDate = new DateWithZeroTime(); + const expiringSoonDate = new ExpirationThresholdDate(currentDate.getTime()); + const currentDateWithZeroTime = new DateWithZeroTime(); + const currentDateWithDayEndTime = new DateWithDayEndTime(); + + // 第五のアカウントを取得 + const accountRepository = datasource.getRepository(Account); + const accounts = await accountRepository.find({ + where: { + tier: TIERS.TIER5, + }, + relations: { + primaryAdminUser: true, + secondaryAdminUser: true, + }, + }); + + const sendTargetAccounts = [] as accountInfo[]; + const licenseRepository = datasource.getRepository(License); for (const account of accounts) { // 有効期限がしきい値より未来または未設定で、割り当て可能なライセンス数の取得を行う const allocatableLicenseWithMargin = await licenseRepository.count({ @@ -109,6 +239,7 @@ export async function licenseAlertProcessing( let primaryAdminExternalId: string | undefined; let secondaryAdminExternalId: string | undefined; let parentCompanyName: string | undefined; + if (shortage !== 0 || userCount !== 0) { primaryAdminExternalId = account.primaryAdminUser ? account.primaryAdminUser.external_id @@ -143,12 +274,33 @@ export async function licenseAlertProcessing( secondaryAdminEmail: undefined, }); } - }; - await counts(); + return sendTargetAccounts; + } catch (e) { + context.error(e); + context.log("getAlertMailTargetAccount failed."); + throw e; + } finally { + context.log("[OUT]getAlertMailTargetAccount"); + } +} +/** + * Azure AD B2Cからユーザ情報を取得し、アカウント情報を作成する + * @param context + * @param redisClient + * @param adb2c + * @param sendTargetAccounts RDBから取得したアカウント情報 + * @returns accountInfo[] メール送信対象のアカウント情報 + */ +async function createAccountInfo( + context: InvocationContext, + redisClient: RedisClient, + adb2c: AdB2cService, + sendTargetAccounts: accountInfo[] +): Promise { // ADB2Cからユーザーを取得する用の外部ID配列を作成 const externalIds = [] as string[]; - sendTargetAccounts.map((x) => { + sendTargetAccounts.forEach((x) => { if (x.primaryAdminExternalId) { externalIds.push(x.primaryAdminExternalId); } @@ -156,11 +308,10 @@ export async function licenseAlertProcessing( externalIds.push(x.secondaryAdminExternalId); } }); - const adb2cUsers = await adb2c.getUsers(context, externalIds); - if (!adb2cUsers) { + const adb2cUsers = await adb2c.getUsers(context, redisClient, externalIds); + if (adb2cUsers.length === 0) { context.log("Target user not found"); - context.log("[OUT]licenseAlertProcessing"); - return; + return []; } // ADB2Cから取得したメールアドレスをRDBから取得した情報にマージ sendTargetAccounts.map((info) => { @@ -188,17 +339,54 @@ export async function licenseAlertProcessing( } } }); + return sendTargetAccounts; +} + +/** + * アラートメールを送信する + * @param context + * @param redisClient + * @param sendgrid + * @param sendTargetAccounts メール送信対象のアカウント情報 + * @param formattedDate redisのキーに使用する日付 + * @returns ユーザ情報 + */ +async function sendAlertMail( + context: InvocationContext, + redisClient: RedisClient, + sendgrid: SendGridService, + sendTargetAccounts: accountInfo[], + formattedDate: string +): Promise { + try { + context.log("[IN]sendAlertMail"); + + // redis用 + const getAsync = promisify(redisClient.get).bind(redisClient); + const setexAsync = promisify(redisClient.setex).bind(redisClient); + const ttl = process.env.ADB2C_CACHE_TTL; + const mailFrom = getMailFrom(); - const sendMail = async () => { for (const targetAccount of sendTargetAccounts) { // プライマリ管理者が入っているかチェック // 入っていない場合は、アラートメールを送信する必要が無いため、何も処理をせず次のループへ if (targetAccount.primaryAdminExternalId) { // メール送信 // strictNullChecks対応 - if (targetAccount.primaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { + if (!targetAccount.primaryAdminEmail) { + continue; + } + // ライセンス不足メール + if (targetAccount.shortage !== 0) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U103 + ) + ); + if (mailResult !== DONE) { const { subject, text, html } = await createMailContentOfLicenseShortage( targetAccount.companyName, @@ -217,45 +405,107 @@ export async function licenseAlertProcessing( context.log( `Shortage mail send success. mail to :${targetAccount.primaryAdminEmail}` ); - } catch { + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U103 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.primaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); context.log( `Shortage mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); - } - - // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 - if (targetAccount.secondaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { - const { subject, text, html } = - await createMailContentOfLicenseShortage( - targetAccount.companyName, - targetAccount.shortage, - targetAccount.parentCompanyName - ); - // メールを送信 - try { - await sendgrid.sendMail( - targetAccount.secondaryAdminEmail, - mailFrom, - subject, - text, - html - ); - context.log( - `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}` - ); - } catch { - context.log( - `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}` - ); - } - } + throw e; } } - // ライセンス失効警告メール - if (targetAccount.userCountOfLicenseExpiringSoon !== 0) { + // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 + if ( + targetAccount.secondaryAdminEmail && + targetAccount.secondaryAdminExternalId + ) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U103 + ) + ); + if (mailResult !== DONE) { + const { subject, text, html } = + await createMailContentOfLicenseShortage( + targetAccount.companyName, + targetAccount.shortage, + targetAccount.parentCompanyName + ); + // メールを送信 + try { + await sendgrid.sendMail( + targetAccount.secondaryAdminEmail, + mailFrom, + subject, + text, + html + ); + context.log( + `Shortage mail send success. mail to :${targetAccount.secondaryAdminEmail}` + ); + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U103 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.secondaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); + context.log( + `Shortage mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + ); + throw e; + } + } + } + } + + // ライセンス失効警告メール + if (targetAccount.userCountOfLicenseExpiringSoon !== 0) { + // redisに送信履歴がない場合のみ送信する + const mailResult = await getAsync( + makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U104 + ) + ); + if (mailResult !== DONE) { const { subject, text, html } = await createMailContentOfLicenseExpiringSoon( targetAccount.companyName, @@ -274,80 +524,99 @@ export async function licenseAlertProcessing( context.log( `Expiring soon mail send success. mail to :${targetAccount.primaryAdminEmail}` ); - } catch { + // 送信成功時、成功履歴をredisに保存 + try { + const key = makeSendCompKey( + formattedDate, + targetAccount.primaryAdminExternalId, + MAIL_U104 + ); + await setexAsync(key, ttl, DONE); + context.log( + "setex Result:", + `key:${key},ttl:${ttl},value:Done` + ); + } catch (e) { + context.error(e); + context.log( + "setex failed.", + `target: ${targetAccount.primaryAdminEmail}` + ); + } + } catch (e) { + context.error(e); context.log( `Expiring soon mail send failed. mail to :${targetAccount.primaryAdminEmail}` ); + throw e; } + } - // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 - if (targetAccount.secondaryAdminEmail) { - // ライセンス不足メール - if (targetAccount.shortage !== 0) { - const { subject, text, html } = - await createMailContentOfLicenseExpiringSoon( - targetAccount.companyName, - targetAccount.userCountOfLicenseExpiringSoon, - targetAccount.parentCompanyName - ); - // メールを送信 + // セカンダリ管理者が存在する場合、セカンダリ管理者にも送信 + if ( + targetAccount.secondaryAdminEmail && + targetAccount.secondaryAdminExternalId + ) { + // redisに送信履歴がない場合のみ送信する + const mailResult = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U104 + ); + if (mailResult !== DONE) { + const { subject, text, html } = + await createMailContentOfLicenseExpiringSoon( + targetAccount.companyName, + targetAccount.userCountOfLicenseExpiringSoon, + targetAccount.parentCompanyName + ); + // メールを送信 + try { + await sendgrid.sendMail( + targetAccount.secondaryAdminEmail, + mailFrom, + subject, + text, + html + ); + context.log( + `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}` + ); try { - await sendgrid.sendMail( - targetAccount.secondaryAdminEmail, - mailFrom, - subject, - text, - html + const key = makeSendCompKey( + formattedDate, + targetAccount.secondaryAdminExternalId, + MAIL_U104 ); + await setexAsync(key, ttl, DONE); context.log( - `Expiring soon mail send success. mail to :${targetAccount.secondaryAdminEmail}` + "setex Result:", + `key:${key},ttl:${ttl},value:Done` ); - } catch { + } catch (e) { + context.error(e); context.log( - `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + "setex failed.", + `target: ${targetAccount.secondaryAdminEmail}` ); } + } catch (e) { + context.error(e); + context.log( + `Expiring soon mail send failed. mail to :${targetAccount.secondaryAdminEmail}` + ); + throw e; } } } } } } - }; - await sendMail(); - - context.log("[OUT]licenseAlertProcessing"); -} - -export async function licenseAlert( - myTimer: Timer, - context: InvocationContext -): Promise { - context.log("[IN]licenseAlert"); - - dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); - const datasource = new DataSource({ - type: "mysql", - host: process.env.DB_HOST, - port: Number(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [User, Account, License], - }); - await datasource.initialize(); - - const adb2c = new AdB2cService(); - const sendgrid = new SendGridService(); - try { - await licenseAlertProcessing(context, datasource, sendgrid, adb2c); } catch (e) { - context.log("licenseAlertProcessing failed"); - context.error(e); + context.log("sendAlertMail failed."); + throw e; } finally { - await datasource.destroy(); - context.log("[OUT]licenseAlert"); + context.log("[OUT]sendAlertMail"); } } diff --git a/dictation_function/src/functions/redisTimerTest.ts b/dictation_function/src/functions/redisTimerTest.ts deleted file mode 100644 index 419079d..0000000 --- a/dictation_function/src/functions/redisTimerTest.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { app, InvocationContext, Timer } from "@azure/functions"; -import * as dotenv from "dotenv"; -import { promisify } from "util"; -import { createRedisClient } from "../redis/redis"; - -export async function redisTimerTest( - myTimer: Timer, - context: InvocationContext -): Promise { - context.log("---Timer function processed request."); - - dotenv.config({ path: ".env" }); - dotenv.config({ path: ".env.local", override: true }); - - const redisClient = createRedisClient(); - const setAsync = promisify(redisClient.set).bind(redisClient); - const getAsync = promisify(redisClient.get).bind(redisClient); - - await setAsync("foo", "bar"); - const value = await getAsync("foo"); - context.log(`value=${value}`); // returns 'bar' - - await redisClient.quit; -} - -app.timer("redisTimerTest", { - schedule: "*/30 * * * * *", - handler: redisTimerTest, -}); diff --git a/dictation_function/src/redis/redis.ts b/dictation_function/src/redis/redis.ts index aedb573..c4ef775 100644 --- a/dictation_function/src/redis/redis.ts +++ b/dictation_function/src/redis/redis.ts @@ -20,12 +20,34 @@ export const createRedisClient = (): RedisClient => { host: host, port: port, password: password, + retry_strategy: (options) => { + if (options.attempt <= 3) { + console.log( + `Retrying connection to Redis. Attempt ${options.attempt}` + ); + return 10000; // ミリ秒単位でのリトライまでの間隔 + } else { + console.log("Exceeded maximum number of connection attempts."); + return undefined; // リトライを終了 + } + }, }); } else { client = createClient({ url: `rediss://${host}:${port}`, password: password, tls: {}, + retry_strategy: (options) => { + if (options.attempt <= 3) { + console.log( + `Retrying connection to Redis. Attempt ${options.attempt}` + ); + return 10000; // ミリ秒単位でのリトライまでの間隔 + } else { + console.log("Exceeded maximum number of connection attempts."); + return undefined; // リトライを終了 + } + }, }); } diff --git a/dictation_function/src/test/licenseAlert.spec.ts b/dictation_function/src/test/licenseAlert.spec.ts index a37e9d4..d790cf0 100644 --- a/dictation_function/src/test/licenseAlert.spec.ts +++ b/dictation_function/src/test/licenseAlert.spec.ts @@ -13,6 +13,8 @@ import { ADB2C_SIGN_IN_TYPE } from "../constants"; import { SendGridService } from "../sendgrid/sendgrid"; import { AdB2cService } from "../adb2c/adb2c"; import { InvocationContext } from "@azure/functions"; +import { RedisClient } from "redis"; +import { createRedisClient } from "../redis/redis"; describe("licenseAlert", () => { dotenv.config({ path: ".env" }); @@ -40,6 +42,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -63,8 +66,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(1); + redisClient.quit; }); it("ライセンス在庫不足メール、ライセンス失効警告メールが送信されること", async () => { @@ -72,6 +82,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -96,8 +107,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(2); + redisClient.quit; }); it("在庫があるため、ライセンス在庫不足メールが送信されないこと", async () => { @@ -105,6 +123,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -142,8 +161,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(0); + redisClient.quit; }); it("AutoRenewがtureのため、ライセンス失効警告メールが送信されないこと", async () => { @@ -151,6 +177,7 @@ describe("licenseAlert", () => { const context = new InvocationContext(); const sendgridMock = new SendGridServiceMock() as SendGridService; const adb2cMock = new AdB2cServiceMock() as AdB2cService; + const redisClient = createRedisClient(); // 呼び出し回数でテスト成否を判定 const spySend = jest.spyOn(sendgridMock, "sendMail"); @@ -175,8 +202,15 @@ describe("licenseAlert", () => { null ); - await licenseAlertProcessing(context, source, sendgridMock, adb2cMock); + await licenseAlertProcessing( + context, + source, + redisClient, + sendgridMock, + adb2cMock + ); expect(spySend.mock.calls).toHaveLength(1); + redisClient.quit; }); }); @@ -211,6 +245,7 @@ export class AdB2cServiceMock { */ async getUsers( context: InvocationContext, + redisClient: RedisClient, externalIds: string[] ): Promise { const AdB2cMockUsers: AdB2cUser[] = [