Skip to content

# 283 SlackのDM送信を5分ごとに実行する処理の追加#322

Open
taminororo wants to merge 6 commits into
developfrom
feat/kanba/286-regular-execution-of-notif
Open

# 283 SlackのDM送信を5分ごとに実行する処理の追加#322
taminororo wants to merge 6 commits into
developfrom
feat/kanba/286-regular-execution-of-notif

Conversation

@taminororo

@taminororo taminororo commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

対応Issue

resolve #271

概要

未送信ログを送信する NotificationUseCase.ProcessUnsentNotifications を、API プロセス内で5分間隔に自動実行する scheduler を追加した。

これまでは cmd/send-notifications を手動で実行した瞬間だけ通知が処理され、action_logsis_sent=false のログが溜まり続けていた。このPRで、API を起動しておくだけで未送信ログが5分ごとに自動で Slack へ送られるようになる。#254 の手動実行を自動化したものにあたる。

設計は3つのファイルに分かれる。

  • externals/scheduler は新規ファイルで、ticker ループで Job を goroutine として回す。Job を func(ctx) error の関数型で受け取ることで、scheduler から usecase を import せずに済み、疎結合を保てる
  • di.goNotificationUseCase を組み立て、server.RunServer の直前に scheduler.New("notification", 5*time.Minute, uc.ProcessUnsentNotifications).Start(ctx) を呼ぶ。Start は goroutine を起動してすぐ return する
  • AGENTS.md に、API を単一インスタンス前提で動かすことを明記した

テスト項目

事前準備として次を行う。

  • SLACK_BOT_TOKEN を env に設定する。値は Slack App の OAuth ページで取得できる
  • 通知を受け取るユーザーの users.slack_user_id に自分の Slack member ID を設定する。ここが空だと DM の送り先が無い
  • make up で API と DB を起動する。Mac では mac-up を使う

そのうえで、自動送信を確認する。ここがこのPRの主眼になる。

  • シフトを変更するなどして action_logsis_sent=false のログを1件作る
  • 手動コマンドを実行せずに最大5分待つと、bot から DM が届く
  • もう1件未送信ログを作り、次の tick で自動送信されることを確認する。5分以内に送られれば定期実行できている
  • 送信後に action_logsis_senttrue へ更新されていることを確認する
  • 5分待つのが手間なら、di.go5*time.Minute を一時的に 30*time.Second などへ下げて周期を素早く確認し、確認後に戻す
  • 既存の手動実行 docker exec -it nutfes-seeft-api sh -c "cd /app && go run ./cmd/send-notifications" が引き続き動くことも確認する

備考

  • API は単一インスタンス前提で動かす。複数レプリカで動かすと各プロセスの ticker が同じログを二重送信するため。本番は1レプリカ運用なので問題ない
  • graceful shutdown への対応は今回のスコープ外とした。Start が ctx を受け取る形にしてあるので、将来 context.WithCancel を渡せば後付けできる
  • 自動テストは入れていない。本物の Slack 送信は外部サービスと時間に依存し CI で回しにくいため、上記の手動確認で代替する

Summary by CodeRabbit

リリースノート

  • 新機能
    • 未送信の通知が5分間隔で自動的にSlack DMに配信されるようになりました。これにより、通知の送信漏れを自動で補完し、ユーザーへのメッセージ配信の確実性が向上しました。

@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

汎用スケジューラパッケージ api/lib/externals/scheduler を新規追加し、Job 型・Scheduler 構造体・NewStart を実装。DI層で Slack サービスと NotificationUseCase を組み立て、5分間隔で ProcessUnsentNotifications を実行するスケジューラを起動する配線を追加。AGENTS.md にアーキテクチャ説明を追記。

Changes

通知定期実行スケジューラ

Layer / File(s) Summary
汎用スケジューラの実装
api/lib/externals/scheduler/scheduler.go
Job 関数型、Scheduler 構造体、New コンストラクタ、Start(ticker goroutine)を新規実装。各 tick で job を呼び出し、エラーはログ出力のみでループを継続する。
DI層でのSlackサービス・NotificationUseCase・Scheduler配線
api/lib/di/di.go
context / slack の import を追加し、InitializeServer 内で SlackService を生成して NotificationUseCase を組み立て、notification スケジューラを5分間隔で context.Background() から起動する配線を追加。
AGENTS.md アーキテクチャ記述追加
AGENTS.md
externals 一覧に scheduler を追記し、5分間隔 flush の仕様・単一インスタンス前提・多重起動時の二重送信リスクを説明するセクションを追加。

推定コードレビュー工数

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 ぴょんと goroutine、飛び出して
ticker の音で 5分ごとに
未送信のログを Slack へとぽいっ
エラーが出ても ループは止まらぬ
うさぎのように、休まず届ける 📨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed PRタイトル「283 SlackのDM送信を5分ごとに実行する処理の追加」は、このPRの主な変更内容(5分間隔でのNotificationUseCaseの自動実行)を明確に表しており、pr_objectivesと一致しています。
Description check ✅ Passed PR説明は対応Issue、概要、テスト項目、備考をすべて含み、detailed implementation details、design rationale、およびtest procedures を十分に記載しており、テンプレート要件を満たしています。
Linked Issues check ✅ Passed PR変更はissue #271 のすべての主要な開発要件を満たしており、schedulerパッケージの実装、di.goの配線追加、AGENTS.mdのドキュメント更新、および既存手動実行の互換性保証がすべて実装されています。
Out of Scope Changes check ✅ Passed すべてのコード変更はissue #271 のスコープ内であり、scheduler実装、di.goの配線、ドキュメント更新に限定されており、アウトオブスコープな変更は検出されません。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kanba/286-regular-execution-of-notif

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
AGENTS.md (1)

52-56: ⚡ Quick win

Graceful shutdown 未対応の制約をドキュメント化してください

現在の実装では scheduler の graceful shutdown が未対応です(PR objectives でも明示的に defer されています)。将来の開発者のため、この制約を「Known Transitional Issues」セクションなどに記載することを推奨します。

📝 ドキュメント追記案

Lines 188-196 の「Known Transitional Issues」セクションに以下を追加:

- **Scheduler の graceful shutdown**: 現在 `externals/scheduler` は Context cancellation に未対応。サーバーシャットダウン時に goroutine が即座に停止しない → 将来対応予定
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 52 - 56, The scheduler implementation currently lacks
graceful shutdown support, but this limitation is not documented. Add a note to
the "Known Transitional Issues" section around lines 188-196 in AGENTS.md to
explicitly document that the externals/scheduler does not support Context
cancellation, meaning goroutines may not stop immediately during server
shutdown. This will alert future developers to this constraint and its status as
a deferred item for future implementation.
api/lib/externals/scheduler/scheduler.go (1)

25-37: ⚡ Quick win

起動直後の即時実行を検討してください

現在の実装では ticker の最初の tick(5分後)まで job が実行されません。サーバー起動時に未送信通知が溜まっている場合、最大5分間送信が遅延します。起動直後に一度 job を実行してから定期実行に入る設計も検討できます。

♻️ 起動直後の即時実行案
 func (s *Scheduler) Start(ctx context.Context) {
 	go func() {
+		// 起動直後に一度実行
+		if err := s.job(ctx); err != nil {
+			log.Printf("[scheduler:%s] job error (initial): %v", s.name, err)
+		}
+
 		ticker := time.NewTicker(s.interval)
 		defer ticker.Stop()
 		for {
 			select {
 			case <-ticker.C:
 				if err := s.job(ctx); err != nil {
 					log.Printf("[scheduler:%s] job error: %v", s.name, err)
 				}
 			case <-ctx.Done():
 				log.Printf("[scheduler:%s] stopped: %v", s.name, ctx.Err())
 				return
 			}
 		}
 	}()
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/externals/scheduler/scheduler.go` around lines 25 - 37, The Start
method currently waits for the first ticker tick before executing the job,
causing delays in processing pending notifications on startup. Add an immediate
execution of s.job(ctx) right after creating the ticker and before the for range
ticker.C loop begins. This ensures the job runs immediately when Start is
called, then continues with the regular interval-based execution in the ticker
loop.
api/lib/di/di.go (1)

102-105: 💤 Low value

SLACK_BOT_TOKEN 未設定時のエラーハンドリング

NewSlackService() が失敗すると log.Fatalf でサーバー起動が中断されます。通知機能がコア機能であれば fail-fast は適切ですが、開発環境で Slack 連携なしで起動したい場合は不便です。

開発時は Slack 連携をオプショナルにする(scheduler を起動しない)設計も検討できます。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/di/di.go` around lines 102 - 105, The current implementation uses
log.Fatalf in the Slack service initialization within the NewSlackService()
call, which crashes the entire server if Slack token is not configured. Instead
of failing hard, make Slack integration optional by logging a warning when
NewSlackService() fails and conditionally skip scheduler startup when the slack
service is unavailable. This allows the application to start in development
environments without requiring Slack configuration, while still enabling Slack
functionality when the token is properly set.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/lib/di/di.go`:
- Around line 101-111: The scheduler's Start() method does not properly handle
context cancellation, preventing graceful shutdown. First, modify the ticker
loop in scheduler.go's Start() method to include a select statement that checks
for context cancellation using <-ctx.Done(), ensuring the scheduler stops when
the context is cancelled. Second, replace the context.Background() call in the
di.go initialization with a cancellable context that is properly propagated from
signal handling logic (add signal handling in main.go or server.RunServer to
catch SIGTERM and SIGINT), allowing the scheduler to be gracefully stopped when
the application receives shutdown signals.

In `@api/lib/externals/scheduler/scheduler.go`:
- Around line 25-37: The Start method's goroutine does not respond to Context
cancellation, causing goroutine leaks and preventing graceful shutdown. Modify
the for loop to use a select statement that monitors both ticker.C for job
execution and ctx.Done() for cancellation signals. When the context is cancelled
(ctx.Done() receives a signal), the function should break the loop, allowing the
defer ticker.Stop() to execute and properly clean up resources. This ensures the
goroutine exits when the context is cancelled rather than running indefinitely.

---

Nitpick comments:
In `@AGENTS.md`:
- Around line 52-56: The scheduler implementation currently lacks graceful
shutdown support, but this limitation is not documented. Add a note to the
"Known Transitional Issues" section around lines 188-196 in AGENTS.md to
explicitly document that the externals/scheduler does not support Context
cancellation, meaning goroutines may not stop immediately during server
shutdown. This will alert future developers to this constraint and its status as
a deferred item for future implementation.

In `@api/lib/di/di.go`:
- Around line 102-105: The current implementation uses log.Fatalf in the Slack
service initialization within the NewSlackService() call, which crashes the
entire server if Slack token is not configured. Instead of failing hard, make
Slack integration optional by logging a warning when NewSlackService() fails and
conditionally skip scheduler startup when the slack service is unavailable. This
allows the application to start in development environments without requiring
Slack configuration, while still enabling Slack functionality when the token is
properly set.

In `@api/lib/externals/scheduler/scheduler.go`:
- Around line 25-37: The Start method currently waits for the first ticker tick
before executing the job, causing delays in processing pending notifications on
startup. Add an immediate execution of s.job(ctx) right after creating the
ticker and before the for range ticker.C loop begins. This ensures the job runs
immediately when Start is called, then continues with the regular interval-based
execution in the ticker loop.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 639b2fd0-53b1-4e28-b2ad-41cdfa415504

📥 Commits

Reviewing files that changed from the base of the PR and between 4940875 and c680c76.

📒 Files selected for processing (3)
  • AGENTS.md
  • api/lib/di/di.go
  • api/lib/externals/scheduler/scheduler.go

Comment thread api/lib/di/di.go
Comment on lines +101 to +111
// Scheduler: 5分間隔で未送信通知を flush する(goroutine で起動し即 return)
slackService, err := slack.NewSlackService()
if err != nil {
log.Fatalf("slack init: %v", err)
}
notificationUseCase := usecase.NewNotificationUseCase(
actionLogRepository, slackService,
userRepository, dateRepository, timeRepository,
taskRepository, shiftRepository, weatherRepository,
)
scheduler.New("notification", 5*time.Minute, notificationUseCase.ProcessUnsentNotifications).Start(context.Background())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# InitializeServer の呼び出し元を確認
rg -nP '\bInitializeServer\s*\(' --type=go -C3

Repository: NUTFes/SeeFT

Length of output: 519


🏁 Script executed:

#!/bin/bash
# scheduler.go の実装を確認
fd -e go -p scheduler | head -20

Repository: NUTFes/SeeFT

Length of output: 97


🏁 Script executed:

#!/bin/bash
# scheduler.go の内容を確認
find . -name "scheduler.go" -type f | xargs cat -n | head -100

Repository: NUTFes/SeeFT

Length of output: 1150


🏁 Script executed:

#!/bin/bash
# main.go の全体を確認
cat -n api/main.go

Repository: NUTFes/SeeFT

Length of output: 807


🏁 Script executed:

#!/bin/bash
# signal handling や graceful shutdown のパターンを検索
rg -i "signal|shutdown|cancel|interrupt" --type=go -C2 | head -150

Repository: NUTFes/SeeFT

Length of output: 38


🏁 Script executed:

#!/bin/bash
# ProcessUnsentNotifications のシグネチャを確認
rg -nP 'ProcessUnsentNotifications.*\(' --type=go -A5 | head -40

Repository: NUTFes/SeeFT

Length of output: 1653


🏁 Script executed:

#!/bin/bash
# scheduler の Job 型の使用箇所を確認
rg -nP 'scheduler.New|type Job' --type=go -B2 -A2

Repository: NUTFes/SeeFT

Length of output: 738


scheduler の Start() メソッドが context キャンセルに対応していないため、graceful shutdown が不可能

scheduler.go の Start() メソッド(27-36行)は、渡された context を job に渡すだけで、スケジューラー自体が context のキャンセルをチェック(<-ctx.Done())していません。そのため、context.Background() 以外の cancellable context を渡しても、ticker は無限に動作し続けます。

さらに、main.go にはシグナルハンドリングや graceful shutdown の仕組みがなく、サーバー停止時に scheduler を適切に停止できません。

修正が必要な箇所:

  • scheduler.go の Start() 内の ticker ループに、context キャンセルをチェックする select 文を追加
  • main.go(または server.RunServer)にシグナルハンドリングと context 伝播の仕組みを実装
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/di/di.go` around lines 101 - 111, The scheduler's Start() method does
not properly handle context cancellation, preventing graceful shutdown. First,
modify the ticker loop in scheduler.go's Start() method to include a select
statement that checks for context cancellation using <-ctx.Done(), ensuring the
scheduler stops when the context is cancelled. Second, replace the
context.Background() call in the di.go initialization with a cancellable context
that is properly propagated from signal handling logic (add signal handling in
main.go or server.RunServer to catch SIGTERM and SIGINT), allowing the scheduler
to be gracefully stopped when the application receives shutdown signals.

Comment on lines +25 to +37
// Start は ticker ループを goroutine で起動し、即座に return する。
// interval ごとに job を実行し、job が返したエラーはログ出力のみ(ループは止めない)。
func (s *Scheduler) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for range ticker.C {
if err := s.job(ctx); err != nil {
log.Printf("[scheduler:%s] job error: %v", s.name, err)
}
}
}()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Context のキャンセルに対応していない(goroutine リーク)

for range ticker.C は Context がキャンセルされても終了しないため、goroutine がリークします。サーバーのシャットダウン時に ticker が停止せず、graceful shutdown が実装できません。

🔒 Context キャンセル対応の修正案
 func (s *Scheduler) Start(ctx context.Context) {
 	go func() {
 		ticker := time.NewTicker(s.interval)
 		defer ticker.Stop()
-		for range ticker.C {
-			if err := s.job(ctx); err != nil {
-				log.Printf("[scheduler:%s] job error: %v", s.name, err)
-			}
+		for {
+			select {
+			case <-ticker.C:
+				if err := s.job(ctx); err != nil {
+					log.Printf("[scheduler:%s] job error: %v", s.name, err)
+				}
+			case <-ctx.Done():
+				log.Printf("[scheduler:%s] stopped: %v", s.name, ctx.Err())
+				return
+			}
 		}
 	}()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Start は ticker ループを goroutine で起動し、即座に return する。
// interval ごとに job を実行し、job が返したエラーはログ出力のみ(ループは止めない)。
func (s *Scheduler) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for range ticker.C {
if err := s.job(ctx); err != nil {
log.Printf("[scheduler:%s] job error: %v", s.name, err)
}
}
}()
}
// Start は ticker ループを goroutine で起動し、即座に return する。
// interval ごとに job を実行し、job が返したエラーはログ出力のみ(ループは止めない)。
func (s *Scheduler) Start(ctx context.Context) {
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := s.job(ctx); err != nil {
log.Printf("[scheduler:%s] job error: %v", s.name, err)
}
case <-ctx.Done():
log.Printf("[scheduler:%s] stopped: %v", s.name, ctx.Err())
return
}
}
}()
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/externals/scheduler/scheduler.go` around lines 25 - 37, The Start
method's goroutine does not respond to Context cancellation, causing goroutine
leaks and preventing graceful shutdown. Modify the for loop to use a select
statement that monitors both ticker.C for job execution and ctx.Done() for
cancellation signals. When the context is cancelled (ctx.Done() receives a
signal), the function should break the loop, allowing the defer ticker.Stop() to
execute and properly clean up resources. This ensures the goroutine exits when
the context is cancelled rather than running indefinitely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

通知UseCaseの定期実行(5分間隔scheduler)をAPIプロセス内に実装

1 participant