Skip to content

Commit 5af82f5

Browse files
authored
feat(subprocess_server): add fallback to Google Maven mirror (#36365)
* feat(subprocess_server): add fallback to Google Maven mirror when Maven Central fails Implement fallback mechanism to use Google Maven mirror when downloads from Maven Central fail (e.g. 403 Forbidden). This improves reliability of jar downloads by providing an alternative source. * lints * refactor(subprocess_server): extract jar download logic into separate method Move the jar download and caching logic from local_jar method to a new _download_jar_to_cache method to improve code reuse and maintainability
1 parent 82952c8 commit 5af82f5

2 files changed

Lines changed: 101 additions & 18 deletions

File tree

sdks/python/apache_beam/utils/subprocess_server.py

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ class JavaJarServer(SubprocessServer):
281281

282282
MAVEN_CENTRAL_REPOSITORY = 'https://repo.maven.apache.org/maven2'
283283
MAVEN_STAGING_REPOSITORY = 'https://repository.apache.org/content/groups/staging' # pylint: disable=line-too-long
284+
GOOGLE_MAVEN_MIRROR = 'https://maven-central.storage-download.googleapis.com/maven2' # pylint: disable=line-too-long
284285
BEAM_GROUP_ID = 'org.apache.beam'
285286
JAR_CACHE = os.path.expanduser("~/.apache_beam/cache/jars")
286287

@@ -419,6 +420,32 @@ def path_to_beam_jar(
419420
return cls.path_to_maven_jar(
420421
artifact_id, cls.BEAM_GROUP_ID, version, maven_repo, appendix=appendix)
421422

423+
@classmethod
424+
def _download_jar_to_cache(
425+
cls, download_url, cached_jar_path, user_agent=None):
426+
"""Downloads a jar from the given URL to the specified cache path.
427+
428+
Args:
429+
download_url (str): The URL to download from.
430+
cached_jar_path (str): The local path where the jar should be cached.
431+
user_agent (str): The user agent to use when downloading.
432+
"""
433+
try:
434+
url_read = FileSystems.open(download_url)
435+
except ValueError:
436+
if user_agent is None:
437+
user_agent = cls._DEFAULT_USER_AGENT
438+
url_request = Request(download_url, headers={'User-Agent': user_agent})
439+
url_read = urlopen(url_request)
440+
with open(cached_jar_path + '.tmp', 'wb') as jar_write:
441+
shutil.copyfileobj(url_read, jar_write, length=1 << 20)
442+
try:
443+
os.rename(cached_jar_path + '.tmp', cached_jar_path)
444+
except FileNotFoundError:
445+
# A race when multiple programs run in parallel and the cached_jar
446+
# is already moved. Safe to ignore.
447+
pass
448+
422449
@classmethod
423450
def local_jar(cls, url, cache_dir=None, user_agent=None):
424451
"""Returns a local path to the given jar, downloading it if necessary.
@@ -449,25 +476,31 @@ def local_jar(cls, url, cache_dir=None, user_agent=None):
449476
os.makedirs(cache_dir)
450477
# TODO: Clean up this cache according to some policy.
451478
try:
452-
try:
453-
url_read = FileSystems.open(url)
454-
except ValueError:
455-
if user_agent is None:
456-
user_agent = cls._DEFAULT_USER_AGENT
457-
url_request = Request(url, headers={'User-Agent': user_agent})
458-
url_read = urlopen(url_request)
459-
with open(cached_jar + '.tmp', 'wb') as jar_write:
460-
shutil.copyfileobj(url_read, jar_write, length=1 << 20)
461-
try:
462-
os.rename(cached_jar + '.tmp', cached_jar)
463-
except FileNotFoundError:
464-
# A race when multiple programs run in parallel and the cached_jar
465-
# is already moved. Safe to ignore.
466-
pass
479+
cls._download_jar_to_cache(url, cached_jar, user_agent)
467480
except URLError as e:
468-
raise RuntimeError(
469-
f'Unable to fetch remote job server jar at {url}: {e}. If no '
470-
f'Internet access at runtime, stage the jar at {cached_jar}')
481+
# Try Google Maven mirror as fallback if the original URL is from
482+
# Maven Central
483+
if url.startswith(cls.MAVEN_CENTRAL_REPOSITORY):
484+
fallback_url = url.replace(
485+
cls.MAVEN_CENTRAL_REPOSITORY, cls.GOOGLE_MAVEN_MIRROR)
486+
_LOGGER.info(
487+
'Trying Google Maven mirror fallback: %s' % fallback_url)
488+
try:
489+
cls._download_jar_to_cache(fallback_url, cached_jar, user_agent)
490+
_LOGGER.info(
491+
'Successfully downloaded from Google Maven mirror: %s' %
492+
fallback_url)
493+
except URLError as fallback_e:
494+
raise RuntimeError(
495+
f'Unable to fetch remote job server jar at {url}: {e}. '
496+
f'Also failed to fetch from Google Maven mirror at '
497+
f'{fallback_url}: {fallback_e}. '
498+
f'If no Internet access at runtime, stage the jar at '
499+
f'{cached_jar}')
500+
else:
501+
raise RuntimeError(
502+
f'Unable to fetch remote job server jar at {url}: {e}. If no '
503+
f'Internet access at runtime, stage the jar at {cached_jar}')
471504
return cached_jar
472505

473506
@classmethod

sdks/python/apache_beam/utils/subprocess_server_test.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,56 @@ def handle(self):
126126
with open(os.path.join(temp_dir, 'file.jar')) as fin:
127127
self.assertEqual(fin.read(), 'data')
128128

129+
def test_local_jar_fallback_to_google_maven_mirror(self):
130+
"""Test that Google Maven mirror is used as fallback
131+
when Maven Central fails."""
132+
class MavenCentralHandler(socketserver.BaseRequestHandler):
133+
timeout = 1
134+
135+
def handle(self):
136+
# Simulate Maven Central returning 403 Forbidden
137+
self.request.sendall(b'HTTP/1.1 403 Forbidden\n\n')
138+
139+
# Set up Maven Central server (will return 403)
140+
maven_port, = subprocess_server.pick_port(None)
141+
maven_server = socketserver.TCPServer(('localhost', maven_port),
142+
MavenCentralHandler)
143+
maven_thread = threading.Thread(target=maven_server.handle_request)
144+
maven_thread.daemon = True
145+
maven_thread.start()
146+
147+
# Temporarily replace the Maven Central constant to use our test server
148+
original_maven_central = (
149+
subprocess_server.JavaJarServer.MAVEN_CENTRAL_REPOSITORY)
150+
151+
try:
152+
subprocess_server.JavaJarServer.MAVEN_CENTRAL_REPOSITORY = (
153+
f'http://localhost:{maven_port}/maven2')
154+
155+
with tempfile.TemporaryDirectory() as temp_dir:
156+
# Use a Maven Central URL that will trigger the fallback to real
157+
# Google mirror
158+
maven_url = (
159+
f'http://localhost:{maven_port}/maven2/org/apache/beam/'
160+
f'beam-sdks-java-extensions-schemaio-expansion-service/2.63.0/'
161+
f'beam-sdks-java-extensions-schemaio-expansion-service-2.63.0.jar')
162+
163+
# This should fail on our mock Maven Central and fallback to the
164+
# real Google mirror
165+
jar_path = subprocess_server.JavaJarServer.local_jar(
166+
maven_url, temp_dir)
167+
168+
# Verify the file was downloaded successfully (from the real Google
169+
# mirror)
170+
self.assertTrue(os.path.exists(jar_path))
171+
jar_size = os.path.getsize(jar_path)
172+
self.assertTrue(jar_size > 0) # Should have actual content
173+
174+
finally:
175+
# Restore original constants
176+
subprocess_server.JavaJarServer.MAVEN_CENTRAL_REPOSITORY = (
177+
original_maven_central)
178+
129179
@unittest.skipUnless(shutil.which('javac'), 'missing java jdk')
130180
def test_classpath_jar(self):
131181
with tempfile.TemporaryDirectory() as temp_dir:

0 commit comments

Comments
 (0)