Skip to content

Commit e262563

Browse files
committed
chore(hermetic-build): explore parallel generation
1 parent 16312ec commit e262563

File tree

6 files changed

+107
-12
lines changed

6 files changed

+107
-12
lines changed

.dockerignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.git/
2+
.github/
3+
.kokoro/
4+
.vscode/
5+
bazel-*/
6+
googleapis/
7+
**/output/
8+
**/target/
9+
**/*.jar

sdk-platform-java/.cloudbuild/library_generation/cloudbuild-library-generation-integration-test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ steps:
5151
cd /workspace
5252
git clone https://github.com/googleapis/google-cloud-java
5353
cd google-cloud-java
54-
git checkout chore/test-hermetic-build
54+
git checkout chore/test-hermetic-build-parallel
5555
mkdir ../golden
5656
cd ../golden
5757
cp -r ../google-cloud-java/java-apigee-connect .

sdk-platform-java/hermetic_build/library_generation/generate_composed_library.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,18 @@ def _construct_effective_arg(
166166
arguments += ["--destination_path", temp_destination_path]
167167

168168
return arguments
169+
170+
171+
import sys
172+
from io import StringIO
173+
import traceback
174+
175+
176+
def library_generation_worker(config, library_path, library, repo_config):
177+
error_msg = None
178+
try:
179+
generate_composed_library(config, library_path, library, repo_config)
180+
except Exception as e:
181+
error_msg = f"{e}\n{traceback.format_exc()}"
182+
183+
return "", error_msg

sdk-platform-java/hermetic_build/library_generation/generate_library.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ if [ -z "${artifact}" ]; then
115115
artifact=""
116116
fi
117117

118-
temp_destination_path="${output_folder}/temp_preprocessed-$RANDOM"
118+
# Use mktemp to guarantee collision-free unique directories when multiple
119+
# library generation processes run concurrently in a shared output folder
120+
temp_destination_path=$(mktemp -d -p "${output_folder}" temp_preprocessed-XXXXXX)
119121
mkdir -p "${output_folder}/${destination_path}"
120122
if [ -d "${temp_destination_path}" ]; then
121123
# we don't want the preprocessed sources of a previous run

sdk-platform-java/hermetic_build/library_generation/generate_repo.py

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
from common.model.generation_config import GenerationConfig
2121
from common.model.library_config import LibraryConfig
2222
from common.utils.proto_path_utils import ends_with_version
23-
from library_generation.generate_composed_library import generate_composed_library
23+
from library_generation.generate_composed_library import (
24+
generate_composed_library,
25+
library_generation_worker,
26+
)
2427
from library_generation.utils.monorepo_postprocessor import monorepo_postprocessing
2528

2629
from common.model.gapic_config import GapicConfig
@@ -57,14 +60,9 @@ def generate_from_yaml(
5760
)
5861
# copy api definition to output folder.
5962
shutil.copytree(api_definitions_path, repo_config.output_folder, dirs_exist_ok=True)
60-
for library_path, library in repo_config.get_libraries().items():
61-
print(f"generating library {library.get_library_name()}")
62-
generate_composed_library(
63-
config=config,
64-
library_path=library_path,
65-
library=library,
66-
repo_config=repo_config,
67-
)
63+
_generate_libraries_in_parallel(config, repo_config)
64+
sys.stdout = original_stdout
65+
sys.stderr = original_stderr
6866

6967
if not config.is_monorepo() or config.contains_common_protos():
7068
return
@@ -152,3 +150,72 @@ def _get_target_libraries_from_api_path(
152150
target_libraries.append(target_library)
153151
return target_libraries
154152
return []
153+
154+
155+
import os
156+
import sys
157+
import threading
158+
from concurrent.futures import ThreadPoolExecutor, as_completed
159+
160+
161+
class ThreadLocalStream:
162+
"""
163+
Thread-safe interceptor to route print() statements into thread-local buffers.
164+
Necessary because sys.stdout is a global stream; direct threading writes interleave outputs.
165+
"""
166+
167+
def __init__(self, original_stream):
168+
self.original_stream = original_stream
169+
self.local = threading.local()
170+
171+
@property
172+
def buffer(self):
173+
return getattr(self.local, "buffer", None)
174+
175+
def write(self, data):
176+
writer = self.buffer if self.buffer is not None else self.original_stream
177+
writer.write(data)
178+
179+
def flush(self):
180+
if self.buffer is not None:
181+
return
182+
self.original_stream.flush()
183+
184+
185+
original_stdout, original_stderr = sys.stdout, sys.stderr
186+
187+
print_lock = threading.Lock()
188+
189+
190+
def _print_worker_result(lib_name, logs, err):
191+
"""
192+
Atomically prints the buffered output of a worker thread directly to original_stdout,
193+
preventing output interleaving in the console.
194+
"""
195+
print_lock.acquire()
196+
status = "[FAILURE]" if err else "[SUCCESS]"
197+
original_stdout.write(f"\n{'='*40}\n{status} Logs for {lib_name}:\n{'='*40}\n")
198+
original_stdout.write(logs)
199+
if err:
200+
original_stdout.write(f"\nError details:\n{err}\n")
201+
original_stdout.flush()
202+
print_lock.release()
203+
204+
205+
def _generate_libraries_in_parallel(config, repo_config):
206+
cores = os.cpu_count() or 4
207+
executor = ThreadPoolExecutor(max_workers=min(cores, 5))
208+
209+
futures = {
210+
executor.submit(
211+
library_generation_worker, config, path, lib, repo_config
212+
): lib.get_library_name()
213+
for path, lib in repo_config.get_libraries().items()
214+
}
215+
216+
for future in as_completed(futures):
217+
lib_name = futures[future]
218+
logs, err = future.result()
219+
_print_worker_result(lib_name, logs, err)
220+
221+
executor.shutdown()

sdk-platform-java/hermetic_build/library_generation/owlbot/bin/entrypoint.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ echo "...done"
6060

6161
# write or restore pom.xml files
6262
echo "Generating missing pom.xml..."
63-
python3 "${scripts_root}/owlbot/src/fix_poms.py" "${versions_file}" "${is_monorepo}"
63+
# Under parallel multi-library generation, fix_poms.py modifies the shared versions_file.
64+
# We use flock to serialize edits safely across concurrent processes.
65+
flock "${versions_file}" python3 "${scripts_root}/owlbot/src/fix_poms.py" "${versions_file}" "${is_monorepo}"
6466
echo "...done"
6567

6668
# write or restore clirr-ignored-differences.xml

0 commit comments

Comments
 (0)