Repository: mars-project/mars

Issue:
[BUG] `df.copy()` makes execution cache missed
<!--
Thank you for your contribution!

Please review https://github.com/mars-project/mars/blob/master/CONTRIBUTING.rst before opening an issue.
-->

**Describe the bug**

For now, execution cache is enabled if the Mars object is executed, however, once it's copied, the cache will miss.

**To Reproduce**
To help us reproducing this bug, please provide information below:
1. Your Python version
2. The version of Mars you use
3. Versions of crucial packages, such as numpy, scipy and protobuf
4. Full stack of the error.
5. Minimized code to reproduce the error.

```python
In [1]: import mars.dataframe as md                                             

In [2]: import mars.tensor as mt                                                

In [3]: df = md.DataFrame(mt.random.rand(10, 3))

In [6]: import time                                                             

In [7]: def f(x): 
   ...:     print('start') 
   ...:     time.sleep(5) 
   ...:     print('end') 
   ...:     return x + 1 
   ...:           

In [9]: df2 = df.apply(lambda x: x + 1).execute()                                                           
start
end
start
end
start
end
Out[9]: 
          0         1         2
0  1.805854  1.985959  1.435207
1  1.123782  1.577428  1.591312
2  1.664900  1.741015  1.578850
3  1.342908  1.609812  1.309529
4  1.546999  1.973471  1.684855
5  1.077777  1.607217  1.948943
6  1.094053  1.547284  1.529331
7  1.742399  1.783776  1.131940
8  1.776285  1.397483  1.992982
9  1.020209  1.772774  1.941404

In [12]: (df2 * 2).execute()   # execution cache leveraged                                                 
Out[12]: 
          0         1         2
0  3.611707  3.971918  2.870414
1  2.247564  3.154856  3.182624
2  3.329800  3.482030  3.157699
3  2.685815  3.219624  2.619058
4  3.093998  3.946942  3.369709
5  2.155553  3.214434  3.897886
6  2.188106  3.094569  3.058662
7  3.484798  3.567552  2.263881
8  3.552571  2.794965  3.985965
9  2.040419  3.545547  3.882808

In [10]: df3 = df2.copy()                                                       

In [11]: (df3 * 2).execute()   # execution cache missed                                                 
start
end
start
end
start
end
Out[11]: 
          0         1         2
0  3.611707  3.971918  2.870414
1  2.247564  3.154856  3.182624
2  3.329800  3.482030  3.157699
3  2.685815  3.219624  2.619058
4  3.093998  3.946942  3.369709
5  2.155553  3.214434  3.897886
6  2.188106  3.094569  3.058662
7  3.484798  3.567552  2.263881
8  3.552571  2.794965  3.985965
9  2.040419  3.545547  3.882808
```


Discussion:

Resolving patch:
diff --git a/mars/core.py b/mars/core.py
--- a/mars/core.py
+++ b/mars/core.py
@@ -531,15 +531,26 @@ def _view(self):
         return super().copy()
 
     def copy(self):
-        new_op = self.op.copy().reset_key()
+        new_op = self.op.copy()
         if new_op.create_view:
             # if the operand is a view, make it a copy
             new_op._create_view = False
-        new_outs = new_op.new_tileables(self.op.inputs, kws=[t.params for t in self.op.outputs],
-                                        output_limit=len(self.op.outputs),
-                                        **self._data.extra_params)
+        params = []
+        for o in self.op.outputs:
+            param = o.params
+            param['_key'] = o.key
+            param.update(o.extra_params)
+            params.append(param)
+        new_outs = new_op.new_tileables(self.op.inputs, kws=params,
+                                        output_limit=len(params))
         pos = -1
         for i, out in enumerate(self.op.outputs):
+            # create a ref to copied one
+            new_out = new_outs[i]
+            if not hasattr(new_out.data, '_siblings'):
+                new_out.data._siblings = []
+            new_out.data._siblings.append(out)
+
             if self._data is out:
                 pos = i
                 break


Test patch:
diff --git a/mars/tests/test_session.py b/mars/tests/test_session.py
--- a/mars/tests/test_session.py
+++ b/mars/tests/test_session.py
@@ -156,6 +156,24 @@ def testReExecuteSame(self):
 
         np.testing.assert_array_equal(result1, result2)
 
+        # test copy, make sure it will not let the execution cache missed
+        df = md.DataFrame(mt.ones((10, 3), chunk_size=5))
+        executed = [False]
+
+        def add_one(x):
+            if executed[0]:  # pragma: no cover
+                raise ValueError('executed before')
+            return x + 1
+
+        df2 = df.apply(add_one)
+        pd.testing.assert_frame_equal(df2.to_pandas(), pd.DataFrame(np.ones((10, 3)) + 1))
+
+        executed[0] = True
+
+        df3 = df2.copy()
+        df4 = df3 * 2
+        pd.testing.assert_frame_equal(df4.to_pandas(), pd.DataFrame(np.ones((10, 3)) * 4))
+
     def testExecuteBothExecutedAndNot(self):
         data = np.random.random((5, 9))