From 1bbf49d7c1b6a26e8d22cf97f6eead1bfdd7f474 Mon Sep 17 00:00:00 2001
From: Christoph Reiter <reiter.christoph@gmail.com>
Date: Mon, 11 Apr 2022 16:18:11 +0200
Subject: [PATCH] Add a health check which initializes all services

In some cases where services can't be instantiated, either because the implementation
is missing, or some dependency is missing, or there is a cyclic dep then things would
fail only at the time the service is needed, for example in one specific request.

To avoid missing such errors and catch them early add a health check which tries to
build all services.

This allows us to abort a deployment in case a service is broken.
---
 src/HealthCheck/Checks/SymfonyCheck.php | 30 ++++++++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/src/HealthCheck/Checks/SymfonyCheck.php b/src/HealthCheck/Checks/SymfonyCheck.php
index c43d2e4..71138ad 100644
--- a/src/HealthCheck/Checks/SymfonyCheck.php
+++ b/src/HealthCheck/Checks/SymfonyCheck.php
@@ -7,15 +7,20 @@ namespace Dbp\Relay\CoreBundle\HealthCheck\Checks;
 use Dbp\Relay\CoreBundle\HealthCheck\CheckInterface;
 use Dbp\Relay\CoreBundle\HealthCheck\CheckOptions;
 use Dbp\Relay\CoreBundle\HealthCheck\CheckResult;
+use Symfony\Component\DependencyInjection\Container;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
 
 class SymfonyCheck implements CheckInterface
 {
     private $parameters;
+    /** @var ContainerInterface */
+    private $container;
 
-    public function __construct(ParameterBagInterface $parameters)
+    public function __construct(ParameterBagInterface $parameters, ContainerInterface $container)
     {
         $this->parameters = $parameters;
+        $this->container = $container;
     }
 
     public function getName(): string
@@ -23,6 +28,28 @@ class SymfonyCheck implements CheckInterface
         return 'core.symfony';
     }
 
+    private function checkAllServices(): CheckResult
+    {
+        $result = new CheckResult('Check if all Symfony services can be initialized');
+        $result->set(CheckResult::STATUS_SUCCESS);
+
+        // This catches errors like unimplemented interfaces, cyclic dependencies and so on.
+        // Otherwise we would only get those errors when the services are actually needed,
+        // on specific requests/tasks at runtime.
+        $container = $this->container;
+        assert($container instanceof Container);
+        foreach ($container->getServiceIds() as $id) {
+            try {
+                $container->get($id);
+            } catch (\Throwable $e) {
+                $result->set(CheckResult::STATUS_FAILURE, $e->getMessage(), ['exception' => $e]);
+                break;
+            }
+        }
+
+        return $result;
+    }
+
     private function checkAppSecret(): CheckResult
     {
         $result = new CheckResult('APP_SECRET should be set');
@@ -54,6 +81,7 @@ class SymfonyCheck implements CheckInterface
         $results = [];
         $results[] = $this->checkAppSecret();
         $results[] = $this->checkAppDebug();
+        $results[] = $this->checkAllServices();
 
         return $results;
     }
-- 
GitLab