# Buggy code
def groupby_reduce(
    self,
    by,
    axis,
    groupby_args,
    map_func,
    map_args,
    reduce_func=None,
    reduce_args=None,
    numeric_only=True,
    drop=False,
):
    """Apply a Groupby via MapReduce pattern.

    Note: Result length will be the number of unique values in `by`.

    Currently, here is how this is implemented:
    - map phase:
        During the map phase we set `as_index` to True to force the `by` into the
        index for the next phase. We always do this so that the reduce phase has
        complete access of the `by` data without having to shuffle it twice. The map
        function is applied with the arguments provided. The `index` of the
        partitions will become the new `by` column. Sometimes, the name of `by` is
        the same as a data column. In these cases we add "_modin_groupby_" to the
        name of the index. This does not happen when grouping by multiple columns
        because those columns have already been dropped as a requirement.
    - reduce phase:
        During the reduce phase, the `by` data is moved from the `index` into the
        data. The names of those inserted become the `by` for the reduce phase of
        the groupby. Once applied, we drop the columns in all partitions after the
        first so we do not insert the data multiple times. We also avoid inserting
        data when the data from the `by` parameter did not come from this object.
        The columns can be derived externally but the new index must be computed
        post hoc.

    Args:
        by: The query compiler object to groupby.
        axis: The axis to groupby. Must be 0 currently.
        groupby_args: The arguments for the groupby component.
        map_func: The function to perform during the map phase.
        map_args: The arguments for the `map_func`.
        reduce_func: The function to perform during the reduce phase.
        reduce_args: The arguments for `reduce_func`.
        numeric_only: Whether to drop non-numeric columns.
        drop: Whether the data in `by` was dropped.

    Returns:
        A new Query Compiler
    """
    assert isinstance(by, type(self)), (
        "Can only use groupby reduce with another Query Compiler"
    )
    assert axis == 0, "Can only groupby reduce with axis=0"

    if numeric_only:
        qc = self.getitem_column_array(self._modin_frame._numeric_columns(True))
    else:
        qc = self
    first_column = qc.columns[0]
    as_index = groupby_args.get("as_index", True)
    # When drop is False and as_index is False, we do not want to insert the `by`
    # data as a new column in the dataframe. We will drop it.
    drop_by = not drop

    # For simplicity we allow only one function to be passed in if both are the
    # same.
    if reduce_func is None:
        reduce_func = map_func
        reduce_args = map_args

    def _map(df, other):
        # Set `as_index` to True to track the metadata of the grouping object
        # It is used to make sure that between phases we are constructing the
        # right index and placing columns in the correct order.
        groupby_args["as_index"] = True
        other = other.squeeze(axis=axis ^ 1)
        if isinstance(other, pandas.DataFrame):
            df = pandas.concat(
                [df] + [other[[o for o in other if o not in df]]], axis=1
            )
            other = list(other.columns)
        result = map_func(df.groupby(by=other, axis=axis, **groupby_args), **map_args)
        if (
            not isinstance(result.index, pandas.MultiIndex)
            and result.index.name is not None
            and result.index.name in result.columns
        ):
            result.index.name = "{}{}".format("_modin_groupby_", result.index.name)
        return result

    def _reduce(df):
        other_len = len(df.index.names)
        df = df.reset_index(drop=False)
        # See note above about setting `as_index`
        groupby_args["as_index"] = as_index
        if other_len > 1:
            by_part = list(df.columns[0:other_len])
        else:
            by_part = df.columns[0]
        result = reduce_func(
            df.groupby(by=by_part, axis=axis, **groupby_args), **reduce_args
        )
        if (
            not isinstance(result.index, pandas.MultiIndex)
            and result.index.name is not None
            and "_modin_groupby_" in result.index.name
        ):
            result.index.name = result.index.name[len("_modin_groupby_") :]
        # Avoid inserting data after the first partition or if the data did not come
        # from this query compiler.
        if not as_index and (first_column not in df.columns or drop_by):
            return result.drop(columns=by_part)
        return result

    if axis == 0:
        if not as_index and drop:
            new_columns = by.columns.append(qc.columns)
        else:
            new_columns = qc.columns
        new_index = None
    else:
        new_index = self.index
        new_columns = None
    new_modin_frame = qc._modin_frame.groupby_reduce(
        axis,
        by._modin_frame,
        _map,
        _reduce,
        new_columns=new_columns,
        new_index=new_index,
    )
    return self.__constructor__(new_modin_frame)


# Traceback (ray.exceptions.RayTaskError)
Traceback (most recent call last):
File "groupby_test16.py", line 29, in <module>
df1 = gb.prod()
File "/localdisk/gashiman/modin/modin/pandas/groupby.py", line 333, in prod
return self._groupby_reduce(callable, None)
File "/localdisk/gashiman/modin/modin/pandas/groupby.py", line 582, in _groupby_reduce
query_compiler=groupby_qc.groupby_reduce(
File "/localdisk/gashiman/modin/modin/backends/pandas/query_compiler.py", line 1298, in groupby_reduce
new_modin_frame = qc._modin_frame.groupby_reduce(
File "/localdisk/gashiman/modin/modin/engines/base/frame/data.py", line 1259, in groupby_reduce
new_index = self._frame_mgr_cls.get_indices(
File "/localdisk/gashiman/modin/modin/engines/ray/pandas_on_ray/frame/partition_manager.py", line 87, in get_indices
new_idx = ray.get(new_idx)
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/ray/worker.py", line 1515, in get
raise value.as_instanceof_cause()
ray.exceptions.RayTaskError(ValueError): ray::modin.engines.ray.pandas_on_ray.frame.partition.deploy_ray_func() (pid=2588931, ip=10.241.129.19)
File "python/ray/_raylet.pyx", line 427, in ray._raylet.execute_task
File "python/ray/_raylet.pyx", line 449, in ray._raylet.execute_task
ray.exceptions.RayTaskError: ray::modin.engines.ray.pandas_on_ray.frame.axis_partition.deploy_ray_func() (pid=2588931, ip=10.241.129.19)
File "python/ray/_raylet.pyx", line 427, in ray._raylet.execute_task
File "python/ray/_raylet.pyx", line 449, in ray._raylet.execute_task
ray.exceptions.RayTaskError: ray::modin.engines.ray.pandas_on_ray.frame.partition_manager.func() (pid=2588931, ip=10.241.129.19)
File "python/ray/_raylet.pyx", line 463, in ray._raylet.execute_task
File "/localdisk/gashiman/modin/modin/engines/ray/pandas_on_ray/frame/partition_manager.py", line 44, in func
return apply_func(df, other)
File "/localdisk/gashiman/modin/modin/backends/pandas/query_compiler.py", line 1248, in _map
result = map_func(
File "/localdisk/gashiman/modin/modin/pandas/groupby.py", line 332, in callable
return df.prod(**kwargs)
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/groupby.py", line 1371, in f
return self._cython_agg_general(alias, alt=npfunc, **kwargs)
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/generic.py", line 993, in _cython_agg_general
agg_blocks, agg_items = self._cython_agg_blocks(
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/generic.py", line 1022, in _cython_agg_blocks
result, _ = self.grouper.aggregate(
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/ops.py", line 586, in aggregate
return self._cython_operation(
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/ops.py", line 529, in _cython_operation
result = self._aggregate(
File "/nfs/site/home/gashiman/.local/lib/python3.8/site-packages/pandas/core/groupby/ops.py", line 608, in _aggregate
agg_func(result, counts, values, comp_ids, min_count)
File "pandas/_libs/groupby.pyx", line 511, in pandas._libs.groupby._group_prod
File "stringsource", line 658, in View.MemoryView.memoryview_cwrapper
File "stringsource", line 349, in View.MemoryView.memoryview.__cinit__
ValueError: buffer source array is read-only

# Fixed code
def groupby_reduce(
    self,
    by,
    axis,
    groupby_args,
    map_func,
    map_args,
    reduce_func=None,
    reduce_args=None,
    numeric_only=True,
    drop=False,
):
    """Apply a Groupby via MapReduce pattern.

    Note: Result length will be the number of unique values in `by`.

    Currently, here is how this is implemented:
    - map phase:
        During the map phase we set `as_index` to True to force the `by` into the
        index for the next phase. We always do this so that the reduce phase has
        complete access of the `by` data without having to shuffle it twice. The map
        function is applied with the arguments provided. The `index` of the
        partitions will become the new `by` column. Sometimes, the name of `by` is
        the same as a data column. In these cases we add "_modin_groupby_" to the
        name of the index. This does not happen when grouping by multiple columns
        because those columns have already been dropped as a requirement.
    - reduce phase:
        During the reduce phase, the `by` data is moved from the `index` into the
        data. The names of those inserted become the `by` for the reduce phase of
        the groupby. Once applied, we drop the columns in all partitions after the
        first so we do not insert the data multiple times. We also avoid inserting
        data when the data from the `by` parameter did not come from this object.
        The columns can be derived externally but the new index must be computed
        post hoc.

    Args:
        by: The query compiler object to groupby.
        axis: The axis to groupby. Must be 0 currently.
        groupby_args: The arguments for the groupby component.
        map_func: The function to perform during the map phase.
        map_args: The arguments for the `map_func`.
        reduce_func: The function to perform during the reduce phase.
        reduce_args: The arguments for `reduce_func`.
        numeric_only: Whether to drop non-numeric columns.
        drop: Whether the data in `by` was dropped.

    Returns:
        A new Query Compiler
    """
    assert isinstance(by, type(self)), (
        "Can only use groupby reduce with another Query Compiler"
    )
    assert axis == 0, "Can only groupby reduce with axis=0"

    if numeric_only:
        qc = self.getitem_column_array(self._modin_frame._numeric_columns(True))
    else:
        qc = self
    as_index = groupby_args.get("as_index", True)
    # For simplicity we allow only one function to be passed in if both are the
    # same.
    if reduce_func is None:
        reduce_func = map_func
        reduce_args = map_args

    def _map(df, other):
        def compute_map(df, other):
            # Set `as_index` to True to track the metadata of the grouping object
            # It is used to make sure that between phases we are constructing the
            # right index and placing columns in the correct order.
            groupby_args["as_index"] = True
            other = other.squeeze(axis=axis ^ 1)
            if isinstance(other, pandas.DataFrame):
                df = pandas.concat(
                    [df] + [other[[o for o in other if o not in df]]], axis=1
                )
                other = list(other.columns)
            result = map_func(
                df.groupby(by=other, axis=axis, **groupby_args), **map_args
            )
            # The _modin_groupby_ prefix indicates that this is the first partition,
            # and since we may need to insert the grouping data in the reduce phase
            if (
                not isinstance(result.index, pandas.MultiIndex)
                and result.index.name is not None
                and result.index.name in result.columns
            ):
                result.index.name = "{}{}".format("_modin_groupby_", result.index.name)
            return result

        try:
            return compute_map(df, other)
        # This will happen with Arrow buffer read-only errors. We don't want to copy
        # all the time, so this will try to fast-path the code first.
        except ValueError:
            return compute_map(df.copy(), other.copy())

    def _reduce(df):
        def compute_reduce(df):
            other_len = len(df.index.names)
            df = df.reset_index(drop=False)
            # See note above about setting `as_index`
            groupby_args["as_index"] = as_index
            if other_len > 1:
                by_part = list(df.columns[0:other_len])
            else:
                by_part = df.columns[0]
            result = reduce_func(
                df.groupby(by=by_part, axis=axis, **groupby_args), **reduce_args
            )
            if (
                not isinstance(result.index, pandas.MultiIndex)
                and result.index.name is not None
                and "_modin_groupby_" in result.index.name
            ):
                result.index.name = result.index.name[len("_modin_groupby_") :]
            if isinstance(by_part, str) and by_part in result.columns:
                if "_modin_groupby_" in by_part and drop:
                    col_name = by_part[len("_modin_groupby_") :]
                    new_result = result.drop(columns=col_name)
                    new_result.columns = [
                        col_name if "_modin_groupby_" in c else c
                        for c in new_result.columns
                    ]
                    return new_result
                else:
                    return result.drop(columns=by_part)
            return result

        try:
            return compute_reduce(df)
        # This will happen with Arrow buffer read-only errors. We don't want to copy
        # all the time, so this will try to fast-path the code first.
        except ValueError:
            return compute_reduce(df.copy())

    if axis == 0:
        new_columns = qc.columns
        new_index = None
    else:
        new_index = self.index
        new_columns = None
    new_modin_frame = qc._modin_frame.groupby_reduce(
        axis,
        by._modin_frame,
        _map,
        _reduce,
        new_columns=new_columns,
        new_index=new_index,
    )
    return self.__constructor__(new_modin_frame)