# Buggy code
def _determine_zarr_chunks(enc_chunks, var_chunks, ndim):
    """
    Given encoding chunks (possibly None) and variable chunks (possibly None)
    """

    # zarr chunk spec:
    # chunks : int or tuple of ints, optional
    #   Chunk shape. If not provided, will be guessed from shape and dtype.

    # if there are no chunks in encoding and the variable data is a numpy
    # array, then we let zarr use its own heuristics to pick the chunks
    if var_chunks is None and enc_chunks is None:
        return None

    # if there are no chunks in encoding but there are dask chunks, we try to
    # use the same chunks in zarr
    # However, zarr chunks needs to be uniform for each array
    # http://zarr.readthedocs.io/en/latest/spec/v1.html#chunks
    # while dask chunks can be variable sized
    # http://dask.pydata.org/en/latest/array-design.html#chunks
    if var_chunks and enc_chunks is None:
        all_var_chunks = list(product(*var_chunks))
        first_var_chunk = all_var_chunks[0]
        # all but the last chunk have to match exactly
        for this_chunk in all_var_chunks[:-1]:
            if this_chunk != first_var_chunk:
                raise ValueError(
                    "Zarr requires uniform chunk sizes excpet for final chunk."
                    " Variable %r has incompatible chunks. Consider "
                    "rechunking using `chunk()`." % (var_chunks,)
                )
        # last chunk is allowed to be smaller
        last_var_chunk = all_var_chunks[-1]
        for len_first, len_last in zip(first_var_chunk, last_var_chunk):
            if len_last > len_first:
                raise ValueError(
                    "Final chunk of Zarr array must be smaller than first. "
                    "Variable %r has incompatible chunks. Consider rechunking "
                    "using `chunk()`." % var_chunks
                )
        return first_var_chunk

    # from here on, we are dealing with user-specified chunks in encoding
    # zarr allows chunks to be an integer, in which case it uses the same chunk
    # size on each dimension.
    # Here we re-implement this expansion ourselves. That makes the logic of
    # checking chunk compatibility easier

    if isinstance(enc_chunks, integer_types):
        enc_chunks_tuple = ndim * (enc_chunks,)
    else:
        enc_chunks_tuple = tuple(enc_chunks)

    if len(enc_chunks_tuple) != ndim:
        raise ValueError(
            "zarr chunks tuple %r must have same length as "
            "variable.ndim %g" % (enc_chunks_tuple, ndim)
        )

    for x in enc_chunks_tuple:
        if not isinstance(x, int):
            raise TypeError(
                "zarr chunks must be an int or a tuple of ints. "
                "Instead found %r" % (enc_chunks_tuple,)
            )

    # if there are chunks in encoding and the variable data is a numpy array,
    # we use the specified chunks
    if var_chunks is None:
        return enc_chunks_tuple

    # the hard case
    # DESIGN CHOICE: do not allow multiple dask chunks on a single zarr chunk
    # this avoids the need to get involved in zarr synchronization / locking
    # From zarr docs:
    #  "If each worker in a parallel computation is writing to a separate
    #   region of the array, and if region boundaries are perfectly aligned
    #   with chunk boundaries, then no synchronization is required."
    # TODO: incorporate synchronizer to allow writes from multiple dask
    # threads
    if var_chunks and enc_chunks_tuple:
        for zchunk, dchunks in zip(enc_chunks_tuple, var_chunks):
            for dchunk in dchunks:
                if dchunk % zchunk:
                    raise NotImplementedError(
                        "Specified zarr chunks %r would overlap multiple dask "
                        "chunks %r. This is not implemented in xarray yet. "
                        " Consider rechunking the data using "
                        "`chunk()` or specifying different chunks in encoding."
                        % (enc_chunks_tuple, var_chunks)
                    )
        return enc_chunks_tuple

    raise AssertionError("We should never get here. Function logic must be wrong.")


# Traceback (ValueError)
In [1]: import xarray as xr

In [2]: import dask.array as dsa

In [3]: da = xr.DataArray(dsa.random.random((8, 7, 11), chunks=(3, 3, 3)), dims=('x', 'y', 't'))

In [4]: da
Out[4]:
<xarray.DataArray 'da.random.random_sample-1aed3ea2f9dd784ec947cb119459fa56' (x: 8, y: 7, t: 11)>
dask.array<shape=(8, 7, 11), dtype=float64, chunksize=(3, 3, 3)>
Dimensions without coordinates: x, y, t

In [5]: da.data.chunks
Out[5]: ((3, 3, 2), (3, 3, 1), (3, 3, 3, 2))

In [6]: da.to_dataset('varname').to_zarr('/Users/jhamman/workdir/test_chunks.zarr')
/Users/jhamman/anaconda/bin/ipython:1: FutureWarning: the order of the arguments on DataArray.to_dataset has changed; you now need to supply ``name`` as a keyword argument
#!/Users/jhamman/anaconda/bin/python
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-7-32fa9a7d0276> in <module>()
----> 1 da.to_dataset('varname').to_zarr('/Users/jhamman/workdir/test_chunks.zarr')

~/anaconda/lib/python3.6/site-packages/xarray/core/dataset.py in to_zarr(self, store, mode, synchronizer, group, encoding, compute)
1185         from ..backends.api import to_zarr
1186         return to_zarr(self, store=store, mode=mode, synchronizer=synchronizer,
-> 1187                        group=group, encoding=encoding, compute=compute)
1188
1189     def __unicode__(self):

~/anaconda/lib/python3.6/site-packages/xarray/backends/api.py in to_zarr(dataset, store, mode, synchronizer, group, encoding, compute)
856     # I think zarr stores should always be sync'd immediately
857     # TODO: figure out how to properly handle unlimited_dims
--> 858     dataset.dump_to_store(store, sync=True, encoding=encoding, compute=compute)
859
860     if not compute:

~/anaconda/lib/python3.6/site-packages/xarray/core/dataset.py in dump_to_store(self, store, encoder, sync, encoding, unlimited_dims, compute)
1073
1074         store.store(variables, attrs, check_encoding,
-> 1075                     unlimited_dims=unlimited_dims)
1076         if sync:
1077             store.sync(compute=compute)

~/anaconda/lib/python3.6/site-packages/xarray/backends/zarr.py in store(self, variables, attributes, *args, **kwargs)
341     def store(self, variables, attributes, *args, **kwargs):
342         AbstractWritableDataStore.store(self, variables, attributes,
--> 343                                         *args, **kwargs)
344
345     def sync(self, compute=True):

~/anaconda/lib/python3.6/site-packages/xarray/backends/common.py in store(self, variables, attributes, check_encoding_set, unlimited_dims)
366         self.set_dimensions(variables, unlimited_dims=unlimited_dims)
367         self.set_variables(variables, check_encoding_set,
--> 368                            unlimited_dims=unlimited_dims)
369
370     def set_attributes(self, attributes):

~/anaconda/lib/python3.6/site-packages/xarray/backends/common.py in set_variables(self, variables, check_encoding_set, unlimited_dims)
403             check = vn in check_encoding_set
404             target, source = self.prepare_variable(
--> 405                 name, v, check, unlimited_dims=unlimited_dims)
406
407             self.writer.add(source, target)

~/anaconda/lib/python3.6/site-packages/xarray/backends/zarr.py in prepare_variable(self, name, variable, check_encoding, unlimited_dims)
325
326         encoding = _extract_zarr_variable_encoding(
--> 327             variable, raise_on_invalid=check_encoding)
328
329         encoded_attrs = OrderedDict()

~/anaconda/lib/python3.6/site-packages/xarray/backends/zarr.py in _extract_zarr_variable_encoding(variable, raise_on_invalid)
181
182     chunks = _determine_zarr_chunks(encoding.get('chunks'), variable.chunks,
--> 183                                     variable.ndim)
184     encoding['chunks'] = chunks
185     return encoding

~/anaconda/lib/python3.6/site-packages/xarray/backends/zarr.py in _determine_zarr_chunks(enc_chunks, var_chunks, ndim)
87                     "Zarr requires uniform chunk sizes excpet for final chunk."
88                     " Variable %r has incompatible chunks. Consider "
---> 89                     "rechunking using `chunk()`." % (var_chunks,))
90         # last chunk is allowed to be smaller
91         last_var_chunk = all_var_chunks[-1]

ValueError: Zarr requires uniform chunk sizes excpet for final chunk. Variable ((3, 3, 2), (3, 3, 1), (3, 3, 3, 2)) has incompatible chunks. Consider rechunking using `chunk()`.

# Fixed code
def _determine_zarr_chunks(enc_chunks, var_chunks, ndim):
    """
    Given encoding chunks (possibly None) and variable chunks (possibly None)
    """

    # zarr chunk spec:
    # chunks : int or tuple of ints, optional
    #   Chunk shape. If not provided, will be guessed from shape and dtype.

    # if there are no chunks in encoding and the variable data is a numpy
    # array, then we let zarr use its own heuristics to pick the chunks
    if var_chunks is None and enc_chunks is None:
        return None

    # if there are no chunks in encoding but there are dask chunks, we try to
    # use the same chunks in zarr
    # However, zarr chunks needs to be uniform for each array
    # http://zarr.readthedocs.io/en/latest/spec/v1.html#chunks
    # while dask chunks can be variable sized
    # http://dask.pydata.org/en/latest/array-design.html#chunks
    if var_chunks and enc_chunks is None:
        if any(len(set(chunks[:-1])) > 1 for chunks in var_chunks):
            raise ValueError(
                "Zarr requires uniform chunk sizes excpet for final chunk."
                " Variable %r has incompatible chunks. Consider "
                "rechunking using `chunk()`." % (var_chunks,)
            )
        if any((chunks[0] < chunks[-1]) for chunks in var_chunks):
            raise ValueError(
                "Final chunk of Zarr array must be smaller than first. "
                "Variable %r has incompatible chunks. Consider rechunking "
                "using `chunk()`." % var_chunks
            )
        # return the first chunk for each dimension
        return tuple(chunk[0] for chunk in var_chunks)

    # from here on, we are dealing with user-specified chunks in encoding
    # zarr allows chunks to be an integer, in which case it uses the same chunk
    # size on each dimension.
    # Here we re-implement this expansion ourselves. That makes the logic of
    # checking chunk compatibility easier

    if isinstance(enc_chunks, integer_types):
        enc_chunks_tuple = ndim * (enc_chunks,)
    else:
        enc_chunks_tuple = tuple(enc_chunks)

    if len(enc_chunks_tuple) != ndim:
        raise ValueError(
            "zarr chunks tuple %r must have same length as "
            "variable.ndim %g" % (enc_chunks_tuple, ndim)
        )

    for x in enc_chunks_tuple:
        if not isinstance(x, int):
            raise TypeError(
                "zarr chunks must be an int or a tuple of ints. "
                "Instead found %r" % (enc_chunks_tuple,)
            )

    # if there are chunks in encoding and the variable data is a numpy array,
    # we use the specified chunks
    if var_chunks is None:
        return enc_chunks_tuple

    # the hard case
    # DESIGN CHOICE: do not allow multiple dask chunks on a single zarr chunk
    # this avoids the need to get involved in zarr synchronization / locking
    # From zarr docs:
    #  "If each worker in a parallel computation is writing to a separate
    #   region of the array, and if region boundaries are perfectly aligned
    #   with chunk boundaries, then no synchronization is required."
    # TODO: incorporate synchronizer to allow writes from multiple dask
    # threads
    if var_chunks and enc_chunks_tuple:
        for zchunk, dchunks in zip(enc_chunks_tuple, var_chunks):
            for dchunk in dchunks:
                if dchunk % zchunk:
                    raise NotImplementedError(
                        "Specified zarr chunks %r would overlap multiple dask "
                        "chunks %r. This is not implemented in xarray yet. "
                        " Consider rechunking the data using "
                        "`chunk()` or specifying different chunks in encoding."
                        % (enc_chunks_tuple, var_chunks)
                    )
        return enc_chunks_tuple

    raise AssertionError("We should never get here. Function logic must be wrong.")