-
Notifications
You must be signed in to change notification settings - Fork 61
Issue 538: Skip individual split files that already exist #548
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| 0.2.2 | ||
| 0.2.3 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -162,17 +162,12 @@ def split_data(self) -> None: | |
| if not self.output_info.split_dims(): | ||
| raise ValueError('No splitting specified in template.') | ||
|
|
||
| if self.should_skip(): | ||
| metrics.Metrics.counter('file_splitters', 'skipped').inc() | ||
| self.logger.info('Skipping %s, file already split.', | ||
| repr(self.input_path)) | ||
| return | ||
|
|
||
| # Here, we keep a map of open file objects (`outputs`). We need these since | ||
| # each output grib file (named `key`) will include multiple `grb` messages | ||
| # each. By writing data to the cache of open file objects, we can keep a | ||
| # minimal amount of data in memory at a time. | ||
| outputs = dict() | ||
| skipped_keys = set() | ||
| with self._open_grib_locally() as grbs: | ||
| self.logger.info('Splitting & uploading %r...', self.input_path) | ||
| try: | ||
|
|
@@ -188,10 +183,20 @@ def split_data(self) -> None: | |
| 'Variable not found in grib: %s', dim) | ||
| key = self.output_info.formatted_output_path(splits) | ||
|
|
||
| if key in skipped_keys: | ||
| del grb | ||
| continue | ||
|
|
||
| # Append the current grib message to a set number of output files. | ||
| # If the target shard doesn't exist, create it. | ||
| if key not in outputs: | ||
| if self.should_skip_file(key): | ||
| self.logger.info('Skipping %s, file already split.', repr(key)) | ||
| skipped_keys.add(key) | ||
| del grb | ||
| continue | ||
| outputs[key] = FileSystems.create(key) | ||
|
|
||
| outputs[key].write(grb.tostring()) | ||
| outputs[key].flush() | ||
|
|
||
|
|
@@ -202,6 +207,12 @@ def split_data(self) -> None: | |
| finally: | ||
| for out in outputs.values(): | ||
| out.close() | ||
|
|
||
| if not outputs: | ||
| metrics.Metrics.counter('file_splitters', 'skipped').inc() | ||
| self.logger.info('Skipping %s, file already split.', | ||
| repr(self.input_path)) | ||
| else: | ||
| self.logger.info('Split %s into %d files', | ||
| self.input_path, len(outputs)) | ||
|
|
||
|
|
@@ -276,6 +287,7 @@ def split_data(self) -> None: | |
| splits = dict(zip(split_dims, line.split(' '))) | ||
| output_path = self.output_info.formatted_output_path(splits) | ||
| if self.should_skip_file(output_path): | ||
| self.logger.info('Skipping %s, file already split.', repr(output_path)) | ||
| skipped_paths.append(output_path) | ||
| continue | ||
| output_paths.append(output_path) | ||
|
|
@@ -285,6 +297,7 @@ def split_data(self) -> None: | |
| repr(self.input_path), ', '.join(skipped_paths)) | ||
| return | ||
|
|
||
| output_paths_set = set(output_paths) | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| self.logger.info('Performing split.') | ||
| dest = os.path.join(tmpdir, flat_output_template) | ||
|
|
@@ -296,9 +309,17 @@ def split_data(self) -> None: | |
| subprocess.run([grib_copy_cmd, local_file.name, dest], | ||
| check=True) | ||
|
|
||
| files = os.listdir(tmpdir) | ||
| num_files = len(files) | ||
| for f in files: | ||
| # grib_copy generates a file for every message, including ones | ||
| # that were already split. Drop those so copy_dir won't re-upload | ||
| # them (issue #538), then flatten the DELIMITER-encoded names back | ||
| # into their nested directory structure for the recursive copy. | ||
| num_files = 0 | ||
| for f in os.listdir(tmpdir): | ||
| dest_file_path = f'{prefix}{f.replace(delimiter, "/")}' | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's use |
||
| if dest_file_path not in output_paths_set: | ||
| os.remove(os.path.join(tmpdir, f)) | ||
| continue | ||
|
j9sh264 marked this conversation as resolved.
|
||
| num_files += 1 | ||
| if delimiter in f: | ||
| new_rel_path = f.replace(delimiter, '/').lstrip('/') | ||
| new_path = os.path.join(tmpdir, new_rel_path) | ||
|
|
@@ -322,11 +343,6 @@ def split_data(self) -> None: | |
| raise ValueError('No splitting specified in template.') | ||
| if any(dim in self._UNSUPPORTED_DIMENSIONS for dim in self.output_info.split_dims()): | ||
| raise ValueError('Unsupported split dimension (lat, lng).') | ||
| if self.should_skip(): | ||
| metrics.Metrics.counter('file_splitters', 'skipped').inc() | ||
| self.logger.info('Skipping %s, file already split.', | ||
| repr(self.input_path)) | ||
| return | ||
|
|
||
| with self._open_dataset_locally() as dataset: | ||
| if any(split not in dataset.dims and split not in ('variable') for split in self.output_info.split_dims()): | ||
|
|
@@ -344,13 +360,20 @@ def split_data(self) -> None: | |
| iterlists.append(dataset[dim]) | ||
| combinations = itertools.product(*iterlists) | ||
| self.logger.info('Splitting & uploading %r...', self.input_path) | ||
| files_written = 0 | ||
| for comb in combinations: | ||
| selected = comb[0] | ||
| for da in comb[1:]: | ||
| for dim in da.coords: | ||
| selected = selected.sel({dim: getattr(da, dim)}) | ||
| self._write_dataset(selected, filtered_split_dims) | ||
| self.logger.info('Finished splitting & uploading %r.', self.input_path) | ||
| if self._write_dataset(selected, filtered_split_dims): | ||
| files_written += 1 | ||
| if files_written == 0: | ||
| metrics.Metrics.counter('file_splitters', 'skipped').inc() | ||
| self.logger.info('Skipping %s, file already split.', | ||
| repr(self.input_path)) | ||
| else: | ||
| self.logger.info('Finished splitting & uploading %r.', self.input_path) | ||
|
|
||
| @contextmanager | ||
| def _open_dataset_locally(self) -> t.Iterator[xr.Dataset]: | ||
|
|
@@ -359,14 +382,19 @@ def _open_dataset_locally(self) -> t.Iterator[xr.Dataset]: | |
| yield ds | ||
| ds.close() | ||
|
|
||
| def _write_dataset(self, dataset: xr.Dataset, split_dims: t.List[str]) -> None: | ||
| """Write destination NetCDF file in NETCDF4 format.""" | ||
| def _write_dataset(self, dataset: xr.Dataset, split_dims: t.List[str]) -> bool: | ||
| """Write destination NetCDF file in NETCDF4 format. Returns True if the file was written.""" | ||
| # Here, we need to write the file locally, since only the scipy engine supports file objects or | ||
| # returning bytes. Further, the scipy engine does not support NETCDF4 (which is HDF5 compliant). | ||
| # Storing data in HDF5 is advantageous since it allows opening NetCDF files with buffered readers. | ||
| output_path = self._get_output_for_dataset(dataset, split_dims) | ||
| if self.should_skip_file(output_path): | ||
| self.logger.info('Skipping %s, file already split.', repr(output_path)) | ||
| return False | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here, let's log these skipped files :) Alternatively, we can also add a log in the |
||
| with tempfile.NamedTemporaryFile() as tmp: | ||
| dataset.to_netcdf(path=tmp.name, engine='netcdf4', format='NETCDF4') | ||
| copy(tmp.name, self._get_output_for_dataset(dataset, split_dims)) | ||
| copy(tmp.name, output_path) | ||
| return True | ||
|
|
||
| def _get_output_for_dataset(self, dataset: xr.Dataset, split_dims: t.List[str]) -> str: | ||
| splits = {'variable': list(dataset.data_vars.keys())[0]} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should log these skipped files to improve pipeline telemetry and simplify troubleshooting downstream.