Repository: jupyterhub/jupyterhub

Issue:
singleuser server version check spams the logs
**Describe the bug**

We have ~277 active single user servers in our deployment right now and on restart of the hub service we see this for each one:

> Mar 4 09:20:45 hub-7bccd48cd5-mp4fk hub [W 2020-03-04 15:20:45.996 JupyterHub _version:56] jupyterhub version 1.2.0dev != jupyterhub-singleuser version 1.1.0. This could cause failure to authenticate and result in redirect loops! 

My only complaint is that logging that per server is redundant and spams the logs. Can we just log that once per restart of the hub?

**To Reproduce**

Have the jupyterhub and jupyterhub-singleuser services at different minor versions.

**Expected behavior**

Just log the warning once since there is no user/server specific context in the message.

**Compute Information**
 - 1.2.0dev - we're running with a custom build based on b4391d0f796864a5b01167701d95eafce3ad987e so that we can pick up the performance fix for issue #2928.


Discussion:

Resolving patch:
diff --git a/jupyterhub/_version.py b/jupyterhub/_version.py
--- a/jupyterhub/_version.py
+++ b/jupyterhub/_version.py
@@ -18,6 +18,15 @@
 
 __version__ = ".".join(map(str, version_info[:3])) + ".".join(version_info[3:])
 
+# Singleton flag to only log the major/minor mismatch warning once per mismatch combo.
+_version_mismatch_warning_logged = {}
+
+
+def reset_globals():
+    """Used to reset globals between test cases."""
+    global _version_mismatch_warning_logged
+    _version_mismatch_warning_logged = {}
+
 
 def _check_version(hub_version, singleuser_version, log):
     """Compare Hub and single-user server versions"""
@@ -42,19 +51,27 @@ def _check_version(hub_version, singleuser_version, log):
         hub_major_minor = V(hub_version).version[:2]
         singleuser_major_minor = V(singleuser_version).version[:2]
         extra = ""
+        do_log = True
         if singleuser_major_minor == hub_major_minor:
             # patch-level mismatch or lower, log difference at debug-level
             # because this should be fine
             log_method = log.debug
         else:
             # log warning-level for more significant mismatch, such as 0.8 vs 0.9, etc.
-            log_method = log.warning
-            extra = " This could cause failure to authenticate and result in redirect loops!"
-        log_method(
-            "jupyterhub version %s != jupyterhub-singleuser version %s." + extra,
-            hub_version,
-            singleuser_version,
-        )
+            key = '%s-%s' % (hub_version, singleuser_version)
+            global _version_mismatch_warning_logged
+            if _version_mismatch_warning_logged.get(key):
+                do_log = False  # We already logged this warning so don't log it again.
+            else:
+                log_method = log.warning
+                extra = " This could cause failure to authenticate and result in redirect loops!"
+                _version_mismatch_warning_logged[key] = True
+        if do_log:
+            log_method(
+                "jupyterhub version %s != jupyterhub-singleuser version %s." + extra,
+                hub_version,
+                singleuser_version,
+            )
     else:
         log.debug(
             "jupyterhub and jupyterhub-singleuser both on version %s" % hub_version


Test patch:
diff --git a/jupyterhub/tests/test_version.py b/jupyterhub/tests/test_version.py
--- a/jupyterhub/tests/test_version.py
+++ b/jupyterhub/tests/test_version.py
@@ -4,6 +4,11 @@
 import pytest
 
 from .._version import _check_version
+from .._version import reset_globals
+
+
+def setup_function(function):
+    reset_globals()
 
 
 @pytest.mark.parametrize(
@@ -25,3 +30,27 @@ def test_check_version(hub_version, singleuser_version, log_level, msg, caplog):
     record = caplog.records[0]
     assert record.levelno == log_level
     assert msg in record.getMessage()
+
+
+def test_check_version_singleton(caplog):
+    """Tests that minor version difference logging is only logged once."""
+    # Run test_check_version twice which will assert that the warning is only logged
+    # once.
+    for x in range(2):
+        test_check_version(
+            '1.2.0',
+            '1.1.0',
+            logging.WARNING,
+            'This could cause failure to authenticate',
+            caplog,
+        )
+    # Run it again with a different singleuser_version to make sure that is logged as
+    # a warning.
+    caplog.clear()
+    test_check_version(
+        '1.2.0',
+        '1.1.1',
+        logging.WARNING,
+        'This could cause failure to authenticate',
+        caplog,
+    )