Repository: dask/dask

Issue:
dask.base.tokenize raises RecursionError when passed an object that contains cycles
**What happened**: `RecursionError` is raised when passing an object containing cycles to `dask.tokenize`

**What you expected to happen**: A valid token to be returned

**Minimal Complete Verifiable Example**:

```python
import dask.base

cycle = dict(a=None)
cycle["a"] = cycle

dask.base.tokenize(cycle)
```

**Anything else we need to know?**:

**Environment**:

- Dask version: 2.20.0
- Python version: 3.8
- Operating System: Ubuntu 18.04 LTS (running under WSL)
- Install method (conda, pip, source): conda (from conda forge)


Discussion:

Patch:
diff --git a/dask/base.py b/dask/base.py
index c05cdc7cc..c2cb0d050 100644
--- a/dask/base.py
+++ b/dask/base.py
@@ -697,7 +697,13 @@ def normalize_set(s):
 
 @normalize_token.register((tuple, list))
 def normalize_seq(seq):
-    return type(seq).__name__, list(map(normalize_token, seq))
+    def func(seq):
+        try:
+            return list(map(normalize_token, seq))
+        except RecursionError:
+            return str(uuid.uuid4())
+
+    return type(seq).__name__, func(seq)
 
 
 @normalize_token.register(literal)
@@ -1004,7 +1010,7 @@ def get_scheduler(get=None, scheduler=None, collections=None, cls=None):
     There are various ways to specify the scheduler to use:
 
     1.  Passing in scheduler= parameters
-    2.  Passing these into global confiuration
+    2.  Passing these into global configuration
     3.  Using defaults of a dask collection
 
     This function centralizes the logic to determine the right scheduler to use


Test patch:
diff --git a/dask/tests/test_base.py b/dask/tests/test_base.py
index 65a22030a..0cda5e864 100644
--- a/dask/tests/test_base.py
+++ b/dask/tests/test_base.py
@@ -460,6 +460,13 @@ def test_tokenize_dense_sparse_array(cls_name):
     assert tokenize(a) != tokenize(b)
 
 
+def test_tokenize_object_with_recursion_error_returns_uuid():
+    cycle = dict(a=None)
+    cycle["a"] = cycle
+
+    assert len(tokenize(cycle)) == 32
+
+
 def test_is_dask_collection():
     class DummyCollection(object):
         def __init__(self, dsk=None):