Repository: lightly-ai/lightly

Issue:
Import times out trying to connect to API?
Simply importing lightly is not working as it hangs on trying to connect to something. Why is that?
Is it really necessary to have outgoing connections for a simple import?

Lightly was installed using pip.

```python
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
<ipython-input-5-28abb08ccc4a> in <module>
      1 import torch
      2 import torchvision
----> 3 import lightly

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/__init__.py in <module>
    109         return _prefetch_generator_available
    110 
--> 111     from lightly.core import *
    112     from lightly import active_learning
    113     from lightly import api

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/core.py in <module>
      6 from lightly.cli.train_cli import _train_cli
      7 from lightly.cli.embed_cli import _embed_cli
----> 8 from lightly.cli.lightly_cli import _lightly_cli
      9 import lightly.cli as cli
     10 

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/cli/__init__.py in <module>
      7 # All Rights Reserved
      8 
----> 9 from lightly.cli.lightly_cli import lightly_cli
     10 from lightly.cli.train_cli import train_cli
     11 from lightly.cli.embed_cli import embed_cli

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/cli/lightly_cli.py in <module>
     13 from lightly.cli.train_cli import _train_cli
     14 from lightly.cli.embed_cli import _embed_cli
---> 15 from lightly.cli.upload_cli import _upload_cli
     16 
     17 

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/cli/upload_cli.py in <module>
     18 
     19 from lightly.api.utils import getenv
---> 20 from lightly.api.api_workflow_client import ApiWorkflowClient
     21 from lightly.data import LightlyDataset
     22 

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/api/__init__.py in <module>
      4 # All Rights Reserved
      5 
----> 6 from lightly.api.api_workflow_client import ApiWorkflowClient

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/api/api_workflow_client.py in <module>
      9 from requests import Response
     10 
---> 11 from lightly.__init__ import __version__
     12 from lightly.api.api_workflow_datasets import _DatasetsMixin
     13 from lightly.api.api_workflow_download_dataset import _DownloadDatasetMixin

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/__init__.py in <module>
    126         from lightly.api.version_checking import pretty_print_latest_version
    127 
--> 128         latest_version = get_latest_version(__version__)
    129         if latest_version is not None:
    130             if version_compare(__version__, latest_version) < 0:

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/api/version_checking.py in get_latest_version(current_version)
     24     try:
     25         versioning_api = get_versioning_api()
---> 26         version_number: str = versioning_api.get_latest_pip_version(current_version = current_version)
     27         return version_number
     28     except Exception as e:

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/api/versioning_api.py in get_latest_pip_version(self, **kwargs)
     53             return self.get_latest_pip_version_with_http_info(**kwargs)  # noqa: E501
     54         else:
---> 55             (data) = self.get_latest_pip_version_with_http_info(**kwargs)  # noqa: E501
     56             return data
     57 

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/api/versioning_api.py in get_latest_pip_version_with_http_info(self, **kwargs)
    109         auth_settings = []  # noqa: E501
    110 
--> 111         return self.api_client.call_api(
    112             '/v1/versions/pip/latest', 'GET',
    113             path_params,

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/api_client.py in call_api(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, async_req, _return_http_data_only, collection_formats, _preload_content, _request_timeout)
    324         """
    325         if not async_req:
--> 326             return self.__call_api(resource_path, method,
    327                                    path_params, query_params, header_params,
    328                                    body, post_params, files,

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/api_client.py in __call_api(self, resource_path, method, path_params, query_params, header_params, body, post_params, files, response_type, auth_settings, _return_http_data_only, collection_formats, _preload_content, _request_timeout)
    156 
    157         # perform request and return response
--> 158         response_data = self.request(
    159             method, url, query_params=query_params, headers=header_params,
    160             post_params=post_params, body=body,

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/api_client.py in request(self, method, url, query_params, headers, post_params, body, _preload_content, _request_timeout)
    346         """Makes the HTTP request using RESTClient."""
    347         if method == "GET":
--> 348             return self.rest_client.GET(url,
    349                                         query_params=query_params,
    350                                         _preload_content=_preload_content,

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/rest.py in GET(self, url, headers, query_params, _preload_content, _request_timeout)
    232     def GET(self, url, headers=None, query_params=None, _preload_content=True,
    233             _request_timeout=None):
--> 234         return self.request("GET", url,
    235                             headers=headers,
    236                             _preload_content=_preload_content,

~/anaconda3/envs/hip/lib/python3.8/site-packages/lightly/openapi_generated/swagger_client/rest.py in request(self, method, url, query_params, headers, body, post_params, _preload_content, _request_timeout)
    205             # For `GET`, `HEAD`
    206             else:
--> 207                 r = self.pool_manager.request(method, url,
    208                                               fields=query_params,
    209                                               preload_content=_preload_content,

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/request.py in request(self, method, url, fields, headers, **urlopen_kw)
     72 
     73         if method in self._encode_url_methods:
---> 74             return self.request_encode_url(
     75                 method, url, fields=fields, headers=headers, **urlopen_kw
     76             )

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/request.py in request_encode_url(self, method, url, fields, headers, **urlopen_kw)
     94             url += "?" + urlencode(fields)
     95 
---> 96         return self.urlopen(method, url, **extra_kw)
     97 
     98     def request_encode_body(

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/poolmanager.py in urlopen(self, method, url, redirect, **kw)
    373             response = conn.urlopen(method, url, **kw)
    374         else:
--> 375             response = conn.urlopen(method, u.request_uri, **kw)
    376 
    377         redirect_location = redirect and response.get_redirect_location()

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/connectionpool.py in urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, **response_kw)
    697 
    698             # Make the request on the httplib connection object.
--> 699             httplib_response = self._make_request(
    700                 conn,
    701                 method,

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/connectionpool.py in _make_request(self, conn, method, url, timeout, chunked, **httplib_request_kw)
    380         # Trigger any extra validation we need to do.
    381         try:
--> 382             self._validate_conn(conn)
    383         except (SocketTimeout, BaseSSLError) as e:
    384             # Py2 raises this as a BaseSSLError, Py3 raises it as socket timeout.

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/connectionpool.py in _validate_conn(self, conn)
   1008         # Force connect early to allow us to validate the connection.
   1009         if not getattr(conn, "sock", None):  # AppEngine might not have  `.sock`
-> 1010             conn.connect()
   1011 
   1012         if not conn.is_verified:

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/connection.py in connect(self)
    351     def connect(self):
    352         # Add certificate verification
--> 353         conn = self._new_conn()
    354         hostname = self.host
    355         tls_in_tls = False

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/connection.py in _new_conn(self)
    167 
    168         try:
--> 169             conn = connection.create_connection(
    170                 (self._dns_host, self.port), self.timeout, **extra_kw
    171             )

~/anaconda3/envs/hip/lib/python3.8/site-packages/urllib3/util/connection.py in create_connection(address, timeout, source_address, socket_options)
     84             if source_address:
     85                 sock.bind(source_address)
---> 86             sock.connect(sa)
     87             return sock
     88 

KeyboardInterrupt: 
```


Discussion:
Hi @luzuku, thanks for reporting this! Indeed, it shouldn't happen that the API request times out. We do the check to see if there is a later version available which could cause incompatabilities.

We will look into it ASAP.
@luzuku 
This is a check if a newer version of lightly exists. If you have an old one it will raise a warning similar to the following:
```
user@machine dir % lightly-train
/Users/python3.9/site-packages/lightly/api/version_checking.py:57: Warning: You are using lightly version 1.1.20. There is a newer version of the package available. For compatability reasons, please upgrade your current version: pip install lightly==1.1.
```


Resolving patch:
diff --git a/lightly/__init__.py b/lightly/__init__.py
--- a/lightly/__init__.py
+++ b/lightly/__init__.py
@@ -80,7 +80,7 @@
 __name__ = 'lightly'
 __version__ = '1.1.21'
 
-from multiprocessing import current_process
+import os
 
 try:
     # See (https://github.com/PyTorchLightning/pytorch-lightning)
@@ -118,16 +118,16 @@ def _is_prefetch_generator_available():
     from lightly import openapi_generated
     from lightly import transforms
     from lightly import utils
+    
+    from lightly.api.version_checking import do_version_check
+
+    if os.getenv('LIGHTLY_DID_VERSION_CHECK', 'False') == 'False':
+        os.environ['LIGHTLY_DID_VERSION_CHECK'] = 'True'
+
+        try:
+            do_version_check(current_version=__version__)
+        except Exception as e:
+            pass
+
 
-    if current_process().name == 'MainProcess':
-        # check for latest version
-        from lightly.api.version_checking import get_latest_version
-        from lightly.api.version_checking import version_compare
-        from lightly.api.version_checking import pretty_print_latest_version
-
-        latest_version = get_latest_version(__version__)
-        if latest_version is not None:
-            if version_compare(__version__, latest_version) < 0:
-                # local version is behind latest version
-                pretty_print_latest_version(latest_version)
 
diff --git a/lightly/api/version_checking.py b/lightly/api/version_checking.py
--- a/lightly/api/version_checking.py
+++ b/lightly/api/version_checking.py
@@ -1,4 +1,6 @@
+import signal
 import warnings
+from multiprocessing import current_process
 from typing import Tuple
 
 import requests
@@ -9,7 +11,36 @@
 from lightly.openapi_generated.swagger_client.configuration import Configuration
 from lightly.api.utils import getenv
 
-from lightly import __version__
+
+class LightlyAPITimeoutException(Exception):
+    pass
+
+class TimeoutDecorator:
+    def __init__(self, seconds):
+        self.seconds = seconds
+
+    def handle_timeout_method(self, *args, **kwargs):
+        raise LightlyAPITimeoutException
+
+    def __enter__(self):
+        signal.signal(signal.SIGALRM, self.handle_timeout_method)
+        signal.alarm(self.seconds)
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        signal.alarm(0)
+
+
+def do_version_check(current_version: str):
+    if current_process().name == 'MainProcess':
+        with TimeoutDecorator(1):
+            versioning_api = get_versioning_api()
+            current_version: str = versioning_api.get_latest_pip_version(
+                current_version=current_version)
+            latest_version: str = versioning_api.get_minimum_compatible_pip_version()
+            if version_compare(current_version, latest_version) < 0:
+                # local version is behind latest version
+                pretty_print_latest_version(current_version, latest_version)
+
 
 
 def get_versioning_api() -> VersioningApi:
@@ -23,7 +54,7 @@ def get_versioning_api() -> VersioningApi:
 def get_latest_version(current_version: str) -> Tuple[None, str]:
     try:
         versioning_api = get_versioning_api()
-        version_number: str = versioning_api.get_latest_pip_version(current_version = current_version)
+        version_number: str = versioning_api.get_latest_pip_version(current_version=current_version)
         return version_number
     except Exception as e:
         return None
@@ -49,8 +80,8 @@ def version_compare(v0, v1):
     return 0
 
 
-def pretty_print_latest_version(latest_version, width=70):
-    warning = f"You are using lightly version {__version__}. " \
+def pretty_print_latest_version(current_version, latest_version, width=70):
+    warning = f"You are using lightly version {current_version}. " \
               f"There is a newer version of the package available. " \
               f"For compatability reasons, please upgrade your current version: " \
               f"pip install lightly=={latest_version}"


Test patch:
diff --git a/tests/api/test_version_checking.py b/tests/api/test_version_checking.py
--- a/tests/api/test_version_checking.py
+++ b/tests/api/test_version_checking.py
@@ -1,8 +1,11 @@
+import sys
+import time
 import unittest
 
 import lightly
 from lightly.api.version_checking import get_latest_version, version_compare, \
-    get_minimum_compatible_version, pretty_print_latest_version
+    get_minimum_compatible_version, pretty_print_latest_version, \
+    LightlyAPITimeoutException, do_version_check
 from tests.api_workflow.mocked_api_workflow_client import MockedVersioningApi
 
 
@@ -27,4 +30,37 @@ def test_get_minimum_compatible_version(self):
         get_minimum_compatible_version()
 
     def test_pretty_print(self):
-        pretty_print_latest_version("1.1.1")
+        pretty_print_latest_version(current_version="curr", latest_version="1.1.1")
+
+    def test_version_check_timout_mocked(self):
+        """
+            We cannot check for other errors as we don't know whether the
+            current LIGHTLY_SERVER_URL is
+            - unreachable (error in < 1 second)
+            - causing a timeout and thus raising a LightlyAPITimeoutException
+            - reachable (success in < 1 second
+
+            Thus this only checks that the actual lightly.do_version_check()
+            with needing >1s internally causes a LightlyAPITimeoutException
+        """
+        try:
+            old_get_versioning_api = lightly.api.version_checking.get_versioning_api
+
+            def mocked_get_versioning_api_timeout():
+                time.sleep(10)
+                print("This line should never be reached, calling sys.exit()")
+                sys.exit()
+
+            lightly.api.version_checking.get_versioning_api = mocked_get_versioning_api_timeout
+
+            start_time = time.time()
+
+            with self.assertRaises(LightlyAPITimeoutException):
+                do_version_check(lightly.__version__)
+
+            duration = time.time() - start_time
+
+            self.assertLess(duration, 1.5)
+
+        finally:
+            lightly.api.version_checking.get_versioning_api = old_get_versioning_api