From 7cc59c81a2df135e059a837c3c901e91a838b916 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:38:40 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E3=83=87=E3=83=A2=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=A7Start()=E9=96=A2=E6=95=B0=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=82=92=E7=90=86=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 8 +++- api/lib/externals/scheduler/scheduler.go | 53 ++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 api/lib/externals/scheduler/scheduler.go diff --git a/AGENTS.md b/AGENTS.md index 0e18b5f8..6f179043 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -36,7 +36,7 @@ api/lib/ ├── internals/ │ ├── controller/ # HTTP I/O(Echo)。DB アクセス禁止 │ └── repository/ # SQL 実行のみ。*sql.Rows を返す -└── externals/{db,server,slack} +└── externals/{db,server,slack,scheduler} # scheduler: 通知の定期実行 ticker ループ mobile/lib/ ├── pages/ # 画面(StatefulWidget) @@ -49,6 +49,12 @@ mobile/lib/ gas/{shift,task,user,rescue}/ # ドメイン別。コード.js / onChange.js 等 ``` +## 通知の定期実行 + +未送信の通知ログ(`action_logs` の `is_sent = false`)は、`externals/scheduler` の ticker ループが API プロセス内で5分間隔に `NotificationUseCase.ProcessUnsentNotifications` を呼び、Slack DM へ flush します(`di.go` で配線。`cmd/send-notifications` は手動 flush 用として併存)。 + +**API は単一インスタンス前提**です。複数レプリカで動かすと各プロセスの ticker が同じ未送信ログを拾って二重送信します。本番(`docker-compose.prod.yml`)は API を1レプリカで運用しているため現状は問題ありません。複数レプリカ化する場合はリーダー選出や排他制御が必要です。 + ## Code Style ### Go (`api/`) diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go new file mode 100644 index 00000000..344c3b7e --- /dev/null +++ b/api/lib/externals/scheduler/scheduler.go @@ -0,0 +1,53 @@ +package scheduler + +import ( + "context" + "log" + "time" +) + +// Job は定期実行する処理の型。 +// +// Go: type Job func(ctx context.Context) error +// Python: Callable[[Context], Awaitable[None]] (typing の型エイリアス) +// TS: type Job = (ctx: Context) => Promise +// +// 関数を「型」として名付けることで、scheduler は usecase を import せずに済む +// (依存方向が di → scheduler / di → usecase の二股になる)。 +type Job func(ctx context.Context) error + +// Scheduler は「名前・間隔・実行する処理」を保持するだけの箱。 +// Python なら @dataclass、TS なら class { constructor(...) } に相当。 +type Scheduler struct { + name string + interval time.Duration + job Job +} + +// New はコンストラクタ。Go には __init__ / constructor が無いので、 +// 慣習として New〜 関数でポインタを返す。 +func New(name string, interval time.Duration, job Job) *Scheduler { + return &Scheduler{name: name, interval: interval, job: job} +} + +// Start は ticker ループを goroutine で起動し、即座に return する。 +// +// ★ここの中身は上林さんが書く。 +// - go func() { ... }() で別の実行単位(goroutine)を起動して呼び出し元をブロックしない +// - time.NewTicker(s.interval) で s.interval ごとに発火するチャネルを作る +// - for ループ内で <-ticker.C を待ち、発火のたびに s.job(ctx) を呼ぶ +// - job がエラーを返したら log.Printf でログ出力のみ(パニックさせない) +// +// 実装したら "log" を import に追加すること(Go は未使用 import をコンパイルエラーにする)。 +func (s *Scheduler) Start(ctx context.Context) { + // TODO(上林): goroutine + time.NewTicker ループを実装する + 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) + } + } + }() +} From 191b35bcbf54b8e27060853a5a9d9f972b03d175 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:39:55 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20di.go=E3=81=AB5=E5=88=86=E3=81=94?= =?UTF-8?q?=E3=81=A8=E3=81=AB=E6=9C=AA=E9=80=81=E4=BF=A1=E3=81=AE=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E3=82=92push=E3=81=99=E3=82=8Bgoroutine=E7=99=BA?= =?UTF-8?q?=E7=81=AB=E3=81=AE=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E6=9B=B8?= =?UTF-8?q?=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/lib/di/di.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/api/lib/di/di.go b/api/lib/di/di.go index b1955654..b0680349 100755 --- a/api/lib/di/di.go +++ b/api/lib/di/di.go @@ -1,9 +1,14 @@ package di import ( + "context" "log" + "time" + "github.com/NUTFes/SeeFT/api/lib/externals/db" + "github.com/NUTFes/SeeFT/api/lib/externals/scheduler" "github.com/NUTFes/SeeFT/api/lib/externals/server" + "github.com/NUTFes/SeeFT/api/lib/externals/slack" "github.com/NUTFes/SeeFT/api/lib/internals/controller" "github.com/NUTFes/SeeFT/api/lib/internals/repository" "github.com/NUTFes/SeeFT/api/lib/internals/repository/abstract" @@ -93,6 +98,18 @@ func InitializeServer() db.Client { reviewController, ) + // 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()) + // Server server.RunServer(router) From de5b9540466ca2b448d6cc4e941758d4d8806fb7 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:40:10 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=E3=83=87=E3=83=A2=E3=82=B3?= =?UTF-8?q?=E3=83=BC=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0=E3=81=97=E3=81=A6?= =?UTF-8?q?=E7=90=86=E8=A7=A3=E3=82=92=E6=B7=B1=E3=82=81=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/cmd/scheduler-demo/main.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 api/cmd/scheduler-demo/main.go diff --git a/api/cmd/scheduler-demo/main.go b/api/cmd/scheduler-demo/main.go new file mode 100644 index 00000000..fd831e68 --- /dev/null +++ b/api/cmd/scheduler-demo/main.go @@ -0,0 +1,31 @@ +// scheduler.Start の挙動を目で見るための使い捨てデモ。確認後に削除する。 +package main + +import ( + "context" + "fmt" + "time" + + "github.com/NUTFes/SeeFT/api/lib/externals/scheduler" +) + +func main() { + fmt.Println("[1] Start を呼ぶ前") + + // 本番では NotificationUseCase.ProcessUnsentNotifications が入る。 + // デモでは Slack も DB も使わず、print だけするダミー job。 + job := func(ctx context.Context) error { + fmt.Println(" → job 実行!(本番ではここで Slack DM を送る)") + return nil + } + + // 間隔は 2 秒(本番は 5*time.Minute)。これが s.interval になる。 + scheduler.New("demo", 2*time.Second, job).Start(context.Background()) + + fmt.Println("[2] Start を呼んだ直後(もうここに来た=ブロックしてない)") + + // 本番では server.RunServer がここでブロックしてプロセスを生かし続ける。 + // デモでは 7 秒だけ待って、裏で job が何回鳴るか観察する。 + time.Sleep(7 * time.Second) + fmt.Println("[3] デモ終了(main が終わると裏方の goroutine も道連れに消える)") +} From 18ca2a11bb0b38beef370758dd2ab8c1174f4e67 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:46:34 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E9=80=9A=E7=9F=A5=E3=81=AE?= =?UTF-8?q?=E6=9C=AA=E9=80=81=E4=BF=A1=E3=83=AD=E3=82=B0=E3=82=925?= =?UTF-8?q?=E5=88=86=E9=96=93=E9=9A=94=E3=81=A7=E8=87=AA=E5=8B=95=E9=80=81?= =?UTF-8?q?=E4=BF=A1=E3=81=99=E3=82=8Bscheduler=E3=82=92=E8=BF=BD=E5=8A=A0?= =?UTF-8?q?(=E3=83=87=E3=83=A2=E3=81=AE=E5=89=8A=E9=99=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/cmd/scheduler-demo/main.go | 31 ------------------------ api/lib/externals/scheduler/scheduler.go | 11 +-------- 2 files changed, 1 insertion(+), 41 deletions(-) delete mode 100644 api/cmd/scheduler-demo/main.go diff --git a/api/cmd/scheduler-demo/main.go b/api/cmd/scheduler-demo/main.go deleted file mode 100644 index fd831e68..00000000 --- a/api/cmd/scheduler-demo/main.go +++ /dev/null @@ -1,31 +0,0 @@ -// scheduler.Start の挙動を目で見るための使い捨てデモ。確認後に削除する。 -package main - -import ( - "context" - "fmt" - "time" - - "github.com/NUTFes/SeeFT/api/lib/externals/scheduler" -) - -func main() { - fmt.Println("[1] Start を呼ぶ前") - - // 本番では NotificationUseCase.ProcessUnsentNotifications が入る。 - // デモでは Slack も DB も使わず、print だけするダミー job。 - job := func(ctx context.Context) error { - fmt.Println(" → job 実行!(本番ではここで Slack DM を送る)") - return nil - } - - // 間隔は 2 秒(本番は 5*time.Minute)。これが s.interval になる。 - scheduler.New("demo", 2*time.Second, job).Start(context.Background()) - - fmt.Println("[2] Start を呼んだ直後(もうここに来た=ブロックしてない)") - - // 本番では server.RunServer がここでブロックしてプロセスを生かし続ける。 - // デモでは 7 秒だけ待って、裏で job が何回鳴るか観察する。 - time.Sleep(7 * time.Second) - fmt.Println("[3] デモ終了(main が終わると裏方の goroutine も道連れに消える)") -} diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go index 344c3b7e..45973545 100644 --- a/api/lib/externals/scheduler/scheduler.go +++ b/api/lib/externals/scheduler/scheduler.go @@ -17,7 +17,6 @@ import ( type Job func(ctx context.Context) error // Scheduler は「名前・間隔・実行する処理」を保持するだけの箱。 -// Python なら @dataclass、TS なら class { constructor(...) } に相当。 type Scheduler struct { name string interval time.Duration @@ -31,16 +30,8 @@ func New(name string, interval time.Duration, job Job) *Scheduler { } // Start は ticker ループを goroutine で起動し、即座に return する。 -// -// ★ここの中身は上林さんが書く。 -// - go func() { ... }() で別の実行単位(goroutine)を起動して呼び出し元をブロックしない -// - time.NewTicker(s.interval) で s.interval ごとに発火するチャネルを作る -// - for ループ内で <-ticker.C を待ち、発火のたびに s.job(ctx) を呼ぶ -// - job がエラーを返したら log.Printf でログ出力のみ(パニックさせない) -// -// 実装したら "log" を import に追加すること(Go は未使用 import をコンパイルエラーにする)。 +// interval ごとに job を実行し、job が返したエラーはログ出力のみ(ループは止めない)。 func (s *Scheduler) Start(ctx context.Context) { - // TODO(上林): goroutine + time.NewTicker ループを実装する go func() { ticker := time.NewTicker(s.interval) defer ticker.Stop() From b2dc29661e959b19b51cc9f358d7aa619cff28d4 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:52:06 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E3=81=AE=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/lib/externals/scheduler/scheduler.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go index 45973545..f8fdb2da 100644 --- a/api/lib/externals/scheduler/scheduler.go +++ b/api/lib/externals/scheduler/scheduler.go @@ -6,17 +6,11 @@ import ( "time" ) -// Job は定期実行する処理の型。 -// -// Go: type Job func(ctx context.Context) error -// Python: Callable[[Context], Awaitable[None]] (typing の型エイリアス) -// TS: type Job = (ctx: Context) => Promise -// -// 関数を「型」として名付けることで、scheduler は usecase を import せずに済む -// (依存方向が di → scheduler / di → usecase の二股になる)。 +// Job は scheduler が定期実行する処理。usecase を import せず関数型で受け取り、 +// scheduler と業務ロジックを疎結合に保つ。 type Job func(ctx context.Context) error -// Scheduler は「名前・間隔・実行する処理」を保持するだけの箱。 +// Scheduler は「名前・間隔・実行する処理」を保持する。 type Scheduler struct { name string interval time.Duration From c680c761157d908deabbdb079e2fd5b87abf5be1 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Wed, 17 Jun 2026 07:54:39 +0900 Subject: [PATCH 6/9] =?UTF-8?q?fix:=20=E4=B8=8D=E8=A6=81=E3=81=AANew?= =?UTF-8?q?=E9=96=A2=E6=95=B0=E3=81=AE=E3=82=B3=E3=83=A1=E3=83=B3=E3=83=88?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/lib/externals/scheduler/scheduler.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go index f8fdb2da..2c4eeee7 100644 --- a/api/lib/externals/scheduler/scheduler.go +++ b/api/lib/externals/scheduler/scheduler.go @@ -17,8 +17,7 @@ type Scheduler struct { job Job } -// New はコンストラクタ。Go には __init__ / constructor が無いので、 -// 慣習として New〜 関数でポインタを返す。 +// New はコンストラクタでSchedulerを生成する。 func New(name string, interval time.Duration, job Job) *Scheduler { return &Scheduler{name: name, interval: interval, job: job} } From 2261d12cbc29b685d1674f3173f2df5dc2706e48 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Tue, 30 Jun 2026 06:21:54 +0900 Subject: [PATCH 7/9] https://go.dev/blog/context --- api/lib/di/di.go | 6 +++--- api/lib/externals/scheduler/scheduler.go | 13 ++++++++++--- api/lib/externals/server/server.go | 24 ++++++++++++++++++++---- api/main.go | 9 ++++++++- 4 files changed, 41 insertions(+), 11 deletions(-) diff --git a/api/lib/di/di.go b/api/lib/di/di.go index b0680349..ff2c9220 100755 --- a/api/lib/di/di.go +++ b/api/lib/di/di.go @@ -16,7 +16,7 @@ import ( "github.com/NUTFes/SeeFT/api/lib/usecase" ) -func InitializeServer() db.Client { +func InitializeServer(ctx context.Context) db.Client { // DB接続 client, err := db.ConnectMySQL() if err != nil { @@ -108,10 +108,10 @@ func InitializeServer() db.Client { userRepository, dateRepository, timeRepository, taskRepository, shiftRepository, weatherRepository, ) - scheduler.New("notification", 5*time.Minute, notificationUseCase.ProcessUnsentNotifications).Start(context.Background()) + scheduler.New("notification", 5*time.Minute, notificationUseCase.ProcessUnsentNotifications).Start(ctx) // Server - server.RunServer(router) + server.RunServer(ctx, router) return client } diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go index 2c4eeee7..38378c4f 100644 --- a/api/lib/externals/scheduler/scheduler.go +++ b/api/lib/externals/scheduler/scheduler.go @@ -28,10 +28,17 @@ 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 } + } }() } diff --git a/api/lib/externals/server/server.go b/api/lib/externals/server/server.go index 1324195a..f340579d 100755 --- a/api/lib/externals/server/server.go +++ b/api/lib/externals/server/server.go @@ -1,16 +1,19 @@ package server import ( + "context" + "net/http" + "os" + "time" + _ "github.com/NUTFes/SeeFT/api/docs" "github.com/NUTFes/SeeFT/api/lib/router" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" echoSwagger "github.com/swaggo/echo-swagger" - "net/http" - "os" ) -func RunServer(router router.Router) { +func RunServer(ctx context.Context, router router.Router) { // echoのインスタンス e := echo.New() @@ -39,5 +42,18 @@ func RunServer(router router.Router) { e.GET("/swagger/*", echoSwagger.WrapHandler) // サーバー起動 - e.Start(":1234") + go func() { + + if err := e.Start(":1234"); err != nil && err != http.ErrServerClosed { + e.Logger.Fatal(err) + } + }() + + <-ctx.Done() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := e.Shutdown(shutdownCtx); err != nil { + e.Logger.Fatal(err) + } } diff --git a/api/main.go b/api/main.go index 3a3b1a0e..d88e2c98 100755 --- a/api/main.go +++ b/api/main.go @@ -1,12 +1,19 @@ package main import ( + "context" + "os" + "os/signal" + "syscall" + "github.com/NUTFes/SeeFT/api/lib/di" _ "github.com/lib/pq" ) func main() { - client := di.InitializeServer() + ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer stop() + client := di.InitializeServer(ctx) defer client.CloseDB() } From 4f63f5bd3c427bbd0e727e4ce66569173d9d191f Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Tue, 30 Jun 2026 06:32:53 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=E3=82=B9=E3=82=B1=E3=82=B8?= =?UTF-8?q?=E3=83=A5=E3=83=BC=E3=83=A9=E8=B5=B7=E5=8B=95=E7=9B=B4=E5=BE=8C?= =?UTF-8?q?=E3=81=AB=20job=20=E3=82=92=E5=8D=B3=E6=99=82=E5=AE=9F=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ticker の初回 tick(5分後)を待たず、起動時に一度 ProcessUnsentNotifications を実行 - 再起動直後に溜まっていた未送信通知の flush 遅延(最大5分)を解消 --- api/lib/externals/scheduler/scheduler.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api/lib/externals/scheduler/scheduler.go b/api/lib/externals/scheduler/scheduler.go index 38378c4f..1e36c6bd 100644 --- a/api/lib/externals/scheduler/scheduler.go +++ b/api/lib/externals/scheduler/scheduler.go @@ -26,6 +26,10 @@ func New(name string, interval time.Duration, job Job) *Scheduler { // interval ごとに job を実行し、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 { From 3c233e3b0ebcade2156ff4c435c7ff06ee34c535 Mon Sep 17 00:00:00 2001 From: taminororo <169162271+taminororo@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:03:08 +0900 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20RunServer/InitializeServer=20?= =?UTF-8?q?=E3=82=92=20error=20=E8=BF=94=E5=8D=B4=E3=81=AB=E3=81=97?= =?UTF-8?q?=E5=BE=8C=E5=A7=8B=E6=9C=AB=E3=81=AE=E3=82=B9=E3=82=AD=E3=83=83?= =?UTF-8?q?=E3=83=97=E3=82=92=E9=98=B2=E6=AD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fatal(os.Exit)は defer を飛ばすため、Start/Shutdown 失敗時に CloseDB 等が走らない問題を解消 - RunServer は error を返し、起動失敗は errCh 経由で受け取る形に - InitializeServer の戻り値を (db.Client, error) にし、db/slack 初期化失敗も伝播 - main を run() error パターンにし、defer 実行後に log.Fatal するよう変更 --- api/lib/di/di.go | 13 +++++++------ api/lib/externals/server/server.go | 17 +++++++++++------ api/main.go | 14 ++++++++++++-- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/api/lib/di/di.go b/api/lib/di/di.go index ff2c9220..ceb71289 100755 --- a/api/lib/di/di.go +++ b/api/lib/di/di.go @@ -2,7 +2,6 @@ package di import ( "context" - "log" "time" "github.com/NUTFes/SeeFT/api/lib/externals/db" @@ -16,11 +15,11 @@ import ( "github.com/NUTFes/SeeFT/api/lib/usecase" ) -func InitializeServer(ctx context.Context) db.Client { +func InitializeServer(ctx context.Context) (db.Client, error) { // DB接続 client, err := db.ConnectMySQL() if err != nil { - log.Fatal("db error") + return nil, err } crud := abstract.NewCrud(client) @@ -101,7 +100,7 @@ func InitializeServer(ctx context.Context) db.Client { // Scheduler: 5分間隔で未送信通知を flush する(goroutine で起動し即 return) slackService, err := slack.NewSlackService() if err != nil { - log.Fatalf("slack init: %v", err) + return nil, err } notificationUseCase := usecase.NewNotificationUseCase( actionLogRepository, slackService, @@ -111,7 +110,9 @@ func InitializeServer(ctx context.Context) db.Client { scheduler.New("notification", 5*time.Minute, notificationUseCase.ProcessUnsentNotifications).Start(ctx) // Server - server.RunServer(ctx, router) + if err := server.RunServer(ctx, router); err != nil { + return client, err + } - return client + return client, nil } diff --git a/api/lib/externals/server/server.go b/api/lib/externals/server/server.go index f340579d..a9157aff 100755 --- a/api/lib/externals/server/server.go +++ b/api/lib/externals/server/server.go @@ -13,7 +13,7 @@ import ( echoSwagger "github.com/swaggo/echo-swagger" ) -func RunServer(ctx context.Context, router router.Router) { +func RunServer(ctx context.Context, router router.Router) error { // echoのインスタンス e := echo.New() @@ -41,19 +41,24 @@ func RunServer(ctx context.Context, router router.Router) { // swagger e.GET("/swagger/*", echoSwagger.WrapHandler) + errCh := make(chan error, 1) + // サーバー起動 go func() { if err := e.Start(":1234"); err != nil && err != http.ErrServerClosed { - e.Logger.Fatal(err) + errCh <- err } }() - <-ctx.Done() + select { + case err := <-errCh: + return err + case <-ctx.Done(): + + } shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - if err := e.Shutdown(shutdownCtx); err != nil { - e.Logger.Fatal(err) - } + return e.Shutdown(shutdownCtx) } diff --git a/api/main.go b/api/main.go index d88e2c98..999f2dbd 100755 --- a/api/main.go +++ b/api/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "log" "os" "os/signal" "syscall" @@ -11,10 +12,19 @@ import ( ) func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} + +func run() error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() - client := di.InitializeServer(ctx) - defer client.CloseDB() + client, err := di.InitializeServer(ctx) + if client != nil { + defer client.CloseDB() + } + return err } // func main() async {