Skip to content

Commit 57f5def

Browse files
committed
Share a session, cache versions, parallel downloads
1 parent f8c7256 commit 57f5def

File tree

3 files changed

+118
-30
lines changed

3 files changed

+118
-30
lines changed

solc_select/repositories.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121
EARLIEST_RELEASE,
2222
LINUX_AMD64,
2323
)
24-
from .infrastructure.http_client import create_http_session
2524
from .models import Platform, SolcVersion
2625

2726

2827
class AbstractSolcRepository(ABC):
2928
"""Abstract base class for Solidity compiler repositories."""
3029

31-
def __init__(self) -> None:
32-
self.session = create_http_session()
30+
def __init__(self, session: requests.Session) -> None:
31+
self.session = session
32+
self._versions_cache: Optional[Dict[str, str]] = None
3333

3434
@property
3535
@abstractmethod
@@ -45,10 +45,17 @@ def list_url(self) -> str:
4545

4646
def get_available_versions(self) -> Dict[str, str]:
4747
"""Get available versions as a dict of version -> artifact_filename."""
48+
# Return cached data if available
49+
if self._versions_cache is not None:
50+
return self._versions_cache
51+
52+
# Fetch from network and cache
4853
response = self.session.get(self.list_url)
4954
response.raise_for_status()
5055
all_releases = response.json()["releases"]
51-
return self._filter_versions(all_releases)
56+
filtered_versions = self._filter_versions(all_releases)
57+
self._versions_cache = filtered_versions
58+
return filtered_versions
5259

5360
def _filter_versions(self, releases: Dict[str, str]) -> Dict[str, str]:
5461
"""Filter versions based on repository-specific criteria.
@@ -94,12 +101,13 @@ def supports_version(self, version: SolcVersion, platform: Platform) -> bool:
94101
class SoliditylangRepository(AbstractSolcRepository):
95102
"""Repository for binaries.soliditylang.org - the main Solidity releases."""
96103

97-
def __init__(self, platform: Platform) -> None:
98-
super().__init__()
104+
def __init__(self, platform: Platform, session: requests.Session) -> None:
105+
super().__init__(session)
99106
self.platform = platform
100107
platform_key = platform.get_soliditylang_key()
101108
self._base_url = f"https://binaries.soliditylang.org/{platform_key}/"
102109
self._list_url = f"https://binaries.soliditylang.org/{platform_key}/list.json"
110+
self._latest_cache: Optional[SolcVersion] = None
103111

104112
@property
105113
def base_url(self) -> str:
@@ -115,17 +123,24 @@ def supports_version(self, version: SolcVersion, platform: Platform) -> bool:
115123

116124
def get_latest_version(self) -> SolcVersion:
117125
"""Get the latest available version."""
126+
# Return cached data if available
127+
if self._latest_cache is not None:
128+
return self._latest_cache
129+
130+
# Fetch from network and cache
118131
response = self.session.get(self.list_url)
119132
response.raise_for_status()
120133
latest_str = response.json()["latestRelease"]
121-
return SolcVersion.parse(latest_str)
134+
latest_version = SolcVersion.parse(latest_str)
135+
self._latest_cache = latest_version
136+
return latest_version
122137

123138

124139
class CryticRepository(AbstractSolcRepository):
125140
"""Repository for crytic/solc - provides additional Linux versions."""
126141

127-
def __init__(self) -> None:
128-
super().__init__()
142+
def __init__(self, session: requests.Session) -> None:
143+
super().__init__(session)
129144

130145
@property
131146
def base_url(self) -> str:
@@ -153,8 +168,8 @@ def supports_version(self, version: SolcVersion, platform: Platform) -> bool:
153168
class AlloyRepository(AbstractSolcRepository):
154169
"""Repository for alloy-rs/solc-builds - provides native ARM64 Darwin binaries."""
155170

156-
def __init__(self) -> None:
157-
super().__init__()
171+
def __init__(self, session: requests.Session) -> None:
172+
super().__init__(session)
158173

159174
@property
160175
def base_url(self) -> str:
@@ -190,19 +205,19 @@ def supports_version(self, version: SolcVersion, platform: Platform) -> bool:
190205
class CompositeRepository:
191206
"""Composite repository that manages multiple underlying repositories."""
192207

193-
def __init__(self, platform: Platform):
208+
def __init__(self, platform: Platform, session: requests.Session):
194209
self.platform = platform
195210
self.repositories: List[AbstractSolcRepository] = []
196211

197212
# Always include the main soliditylang repository
198-
self.repositories.append(SoliditylangRepository(platform))
213+
self.repositories.append(SoliditylangRepository(platform, session))
199214

200215
# Add platform-specific repositories
201216
if platform.get_soliditylang_key() == LINUX_AMD64:
202-
self.repositories.append(CryticRepository())
217+
self.repositories.append(CryticRepository(session))
203218

204219
if platform.os_type == "darwin" and platform.architecture == "arm64":
205-
self.repositories.append(AlloyRepository())
220+
self.repositories.append(AlloyRepository(session))
206221

207222
def get_available_versions(self) -> Dict[str, str]:
208223
"""Get all available versions from all repositories."""

solc_select/services/artifact_manager.py

Lines changed: 84 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,28 +7,31 @@
77

88
import hashlib
99
import os
10+
from concurrent.futures import ThreadPoolExecutor, as_completed
1011
from functools import partial
1112
from io import BufferedRandom
1213
from pathlib import Path
1314
from typing import List
1415
from zipfile import ZipFile
1516

17+
import requests
1618
from Crypto.Hash import keccak
1719

1820
from ..constants import ARTIFACTS_DIR
1921
from ..exceptions import ChecksumMismatchError, SolcSelectError
20-
from ..infrastructure.http_client import create_http_session
2122
from ..models import Platform, SolcArtifact, SolcVersion
2223
from ..repositories import CompositeRepository
2324

2425

2526
class ArtifactManager:
2627
"""Service for managing Solidity compiler artifacts."""
2728

28-
def __init__(self, repository: CompositeRepository, platform: Platform):
29+
def __init__(
30+
self, repository: CompositeRepository, platform: Platform, session: requests.Session
31+
):
2932
self.repository = repository
3033
self.platform = platform
31-
self.session = create_http_session()
34+
self.session = session
3235

3336
def get_installed_versions(self) -> List[SolcVersion]:
3437
"""Get list of installed versions.
@@ -193,13 +196,20 @@ def download_and_install(self, version: SolcVersion, silent: bool = False) -> bo
193196

194197
try:
195198
# Download the file
196-
response = self.session.get(artifact.download_url)
199+
response = self.session.get(artifact.download_url, stream=True)
197200
response.raise_for_status()
198201

199202
# Write and verify the file
200203
with open(artifact.file_path, "w+b", opener=partial(os.open, mode=0o664)) as f:
201-
for chunk in response.iter_content(chunk_size=8192):
202-
f.write(chunk)
204+
try:
205+
for chunk in response.iter_content(chunk_size=8192):
206+
if chunk: # Filter out keep-alive chunks
207+
f.write(chunk)
208+
except KeyboardInterrupt:
209+
# Clean up partially downloaded file on interrupt
210+
if artifact.file_path.exists():
211+
artifact.file_path.unlink(missing_ok=True)
212+
raise
203213

204214
# Verify checksums
205215
self.verify_checksum(artifact, f)
@@ -250,7 +260,7 @@ def _extract_zip_archive(self, artifact: SolcArtifact) -> None:
250260
artifact.file_path.chmod(0o775)
251261

252262
def install_versions(self, versions: List[SolcVersion], silent: bool = False) -> bool:
253-
"""Install multiple versions.
263+
"""Install multiple versions concurrently.
254264
255265
Args:
256266
versions: List of versions to install
@@ -259,15 +269,76 @@ def install_versions(self, versions: List[SolcVersion], silent: bool = False) ->
259269
Returns:
260270
True if all installations succeeded, False otherwise
261271
"""
262-
success = True
272+
if not versions:
273+
return True
263274

264-
for version in versions:
275+
# For single version, use sequential approach
276+
if len(versions) == 1:
265277
try:
266-
if not self.download_and_install(version, silent):
267-
success = False
278+
return self.download_and_install(versions[0], silent)
268279
except SolcSelectError as e:
269280
if not silent:
270281
print(f"Error: {e}")
271-
success = False
282+
return False
283+
284+
# For multiple versions, use parallel approach
285+
if not silent:
286+
print(f"Installing {len(versions)} versions concurrently...")
287+
288+
success_count = 0
289+
total_count = len(versions)
290+
291+
# Use ThreadPoolExecutor with max 5 concurrent downloads
292+
executor = ThreadPoolExecutor(max_workers=5)
293+
future_to_version = {}
294+
295+
try:
296+
# Submit all download jobs
297+
future_to_version = {
298+
executor.submit(self.download_and_install, version, True): version
299+
for version in versions
300+
}
301+
302+
# Process completed downloads
303+
for future in as_completed(future_to_version):
304+
version = future_to_version[future]
305+
try:
306+
result = future.result()
307+
if result:
308+
success_count += 1
309+
if not silent:
310+
print(
311+
f"✓ Version '{version}' installed ({success_count}/{total_count})"
312+
)
313+
elif not silent:
314+
print(
315+
f"✗ Version '{version}' failed to install ({success_count}/{total_count})"
316+
)
317+
except SolcSelectError as e:
318+
if not silent:
319+
print(f"✗ Version '{version}' failed: {e} ({success_count}/{total_count})")
320+
321+
except KeyboardInterrupt:
322+
if not silent:
323+
print(f"\nCancelling installation... ({success_count}/{total_count} completed)")
324+
325+
# Cancel all pending futures
326+
for future in future_to_version:
327+
future.cancel()
328+
329+
# Shutdown executor immediately without waiting for running tasks
330+
executor.shutdown(wait=False)
331+
raise
332+
333+
finally:
334+
# Clean shutdown for normal completion
335+
if not executor._shutdown:
336+
executor.shutdown(wait=True)
337+
338+
if not silent:
339+
if success_count == total_count:
340+
print(f"All {total_count} versions installed successfully!")
341+
else:
342+
print(f"{success_count}/{total_count} versions installed successfully")
272343

273-
return success
344+
return success_count == total_count

solc_select/services/solc_service.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
VersionNotInstalledError,
1919
)
2020
from ..infrastructure.filesystem import FilesystemManager
21+
from ..infrastructure.http_client import create_http_session
2122
from ..models import Platform, SolcVersion
2223
from ..repositories import CompositeRepository
2324
from .artifact_manager import ArtifactManager
@@ -34,9 +35,10 @@ def __init__(self, platform: Optional[Platform] = None):
3435

3536
self.platform = platform
3637
self.filesystem = FilesystemManager()
37-
self.repository = CompositeRepository(platform)
38+
self.session = create_http_session()
39+
self.repository = CompositeRepository(platform, self.session)
3840
self.version_manager = VersionManager(self.repository, platform)
39-
self.artifact_manager = ArtifactManager(self.repository, platform)
41+
self.artifact_manager = ArtifactManager(self.repository, platform, self.session)
4042
self.platform_service = PlatformService(platform)
4143

4244
def get_current_version(self) -> Tuple[Optional[SolcVersion], str]:

0 commit comments

Comments
 (0)