mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2026-10-28 16:18:23 +08:00
`ci/tools/s3_helper.py` (`S3Helper`) and `ci/praktika/s3.py` (`S3`) were
independent S3 clients: separate boto3 client construction, separate local-mode
switch, separate URL construction, separate content-type policy. A fix to one
left the other wrong.
Only five `S3Helper` methods had in-tree callers, and three of them had no
praktika twin at all, so the surviving client grows the two primitives it
lacked:
- `S3.list_prefix(s3_path, recursive=True)` replaces both `list_prefix` and
`list_prefix_non_recursive`; `recursive=False` adds `Delimiter="/"` and
appends `CommonPrefixes`, which is what the non-recursive variant did.
- `S3.get_url(s3_path)` is extracted from the two places `s3.py` already
wrote that expression inline (`copy_file_to_s3`'s tail and `_dump_urls`),
so both now call it.
The remaining nine public `S3Helper` methods had zero callers and go with the
file, along with `compress_files.compress_file_fast`, whose only caller was
`s3_helper`'s >64 MiB zstd branch (unreachable from the one live upload).
Three consequences needed handling rather than inheriting:
`copy_file_from_s3` reports a missing object by returning False, where
`download_file` raised. `create_release.py` discarded the return value at all
three download sites, so an unguarded repoint would have turned a loud release
abort into a silently missing package. Each site now tests the return value and
raises `AssertionError` with the previous message. The test is spelled
`if not ...: raise` rather than `assert`, because an assert whose expression IS
the download deletes the download itself under `python3 -O`: `run()` clears the
package directory outside any assert, and the readiness check that would notice
the empty directory is itself a bare assert, so the step would have reported
success having transferred nothing.
The AutoReleases readiness gate reads the same bucket knob as the CreateRelease
downloader it gates: `S3_BUILDS_BUCKET`, falling back to praktika's
`S3_ARTIFACT_BUCKET`. The two run as separate jobs, so a value set on one runner
and not the other would otherwise leave the checker probing a different bucket
than the producer downloads from, and that drift would surface only at the next
scheduled release.
praktika resolves `Settings` relative to the process working directory
(`SETTINGS_DIRECTORY = "./ci/settings"`). The CherryPick workflow ran
`cherry_pick.py` with `working-directory: ci/tools`, where
`S3_BUCKET_TO_HTTP_ENDPOINT` is None and building a URL raises TypeError, so it
now runs from the repo root as praktika's other entry points already do. A
`sys.path` bootstrap does not fix that; it fixes the import, not the settings
discovery. Sibling imports still resolve because `sys.path[0]` is the script's
own directory, and `git_helper.git_runner` already normalizes to the git root,
so no git operation changes.
Behavioural deltas, all measured old-vs-new at the boto3 seam: the cache
tarball gains `ContentType: application/zstd`, and `clickhouse-builds` URLs
move from path-style to virtual-hosted (both shapes serve the live object).
Changelog category: CI Fix or Improvement (changelog entry is not required)
Related: https://github.com/ClickHouse/ClickHouse/issues/121868
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.0 KiB
Python
118 lines
4.0 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import logging
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
from build_download_helper import DownloadException, download_build_with_progress
|
|
from compress_files import compress_fast, decompress_fast
|
|
from env_helper import S3_BUILDS_BUCKET
|
|
|
|
DOWNLOAD_RETRIES_COUNT = 5
|
|
|
|
|
|
class CacheError(Exception):
|
|
pass
|
|
|
|
|
|
class Cache:
|
|
"""a generic class for all caches
|
|
|
|
`s3` must expose `list_prefix(s3_path, recursive=)`,
|
|
`copy_file_to_s3(s3_path=, local_path=)` and `get_url(s3_path)`, each
|
|
taking one `bucket/key` string (e.g. `ci/praktika`'s `S3`); it is passed
|
|
in so this module stays dependency-free.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
directory: Path,
|
|
temp_path: Path,
|
|
archive_name: str,
|
|
s3,
|
|
):
|
|
self.directory = directory
|
|
self.temp_path = temp_path
|
|
self.archive_name = archive_name
|
|
self.s3 = s3
|
|
|
|
def _download(self, url: str, ignore_error: bool = False) -> None:
|
|
self.temp_path.mkdir(parents=True, exist_ok=True)
|
|
compressed_cache = self.temp_path / self.archive_name
|
|
try:
|
|
if url.startswith("file://"):
|
|
local_s3_cache = Path(url[7:])
|
|
if local_s3_cache.is_file():
|
|
shutil.copy2(local_s3_cache, compressed_cache)
|
|
else:
|
|
logging.warning(
|
|
"The local cache file %s does not exist, creating empty directory",
|
|
local_s3_cache,
|
|
)
|
|
self.directory.mkdir(parents=True, exist_ok=True)
|
|
return
|
|
else:
|
|
download_build_with_progress(url, compressed_cache)
|
|
except DownloadException as e:
|
|
if not ignore_error:
|
|
raise CacheError(f"Failed to download {url}") from e
|
|
logging.warning("Unable downloading cache, creating empty directory")
|
|
self.directory.mkdir(parents=True, exist_ok=True)
|
|
return
|
|
|
|
# decompress the cache and check if the necessary directory is there
|
|
self.directory.parent.mkdir(parents=True, exist_ok=True)
|
|
decompress_fast(compressed_cache, self.directory.parent)
|
|
if not self.directory.exists():
|
|
if not ignore_error:
|
|
raise CacheError(
|
|
"The cache is downloaded and uncompressed, but directory "
|
|
f"{self.directory} does not exist"
|
|
)
|
|
logging.warning(
|
|
"The cache archive was successfully downloaded and "
|
|
"decompressed, but %s does not exitst. Creating empty one",
|
|
self.directory,
|
|
)
|
|
self.directory.mkdir(parents=True, exist_ok=True)
|
|
|
|
def _upload(self, s3_path: str, force_upload: bool = False) -> None:
|
|
if not force_upload:
|
|
existing_cache = self.s3.list_prefix(
|
|
f"{S3_BUILDS_BUCKET}/{s3_path}", recursive=False
|
|
)
|
|
if existing_cache:
|
|
logging.info("Remote cache %s already exist, won't reupload", s3_path)
|
|
return
|
|
|
|
logging.info("Compressing cache")
|
|
archive_path = self.temp_path / self.archive_name
|
|
compress_fast(self.directory, archive_path)
|
|
logging.info("Uploading %s to S3 path %s", archive_path, s3_path)
|
|
self.s3.copy_file_to_s3(
|
|
s3_path=f"{S3_BUILDS_BUCKET}/{s3_path}", local_path=archive_path
|
|
)
|
|
|
|
|
|
class GitHubCache(Cache):
|
|
PREFIX = "ccache/github"
|
|
|
|
def __init__(
|
|
self,
|
|
directory: Path,
|
|
temp_path: Path,
|
|
s3,
|
|
):
|
|
self.force_upload = True
|
|
super().__init__(directory, temp_path, "GitHub.tar.zst", s3)
|
|
self._url = self.s3.get_url(
|
|
f"{S3_BUILDS_BUCKET}/{self.PREFIX}/{self.archive_name}"
|
|
)
|
|
|
|
def download(self):
|
|
logging.info("Searching cache for GitHub class")
|
|
self._download(self._url, True)
|
|
|
|
def upload(self):
|
|
self._upload(f"{self.PREFIX}/{self.archive_name}", True)
|