-
-
Notifications
You must be signed in to change notification settings - Fork 415
feat: implement scoped proxy configurations #431
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
Changes from all commits
9f11bb8
94d9fe5
53f096b
5512f2d
acdb362
9967259
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 |
|---|---|---|
|
|
@@ -16,27 +16,70 @@ import io.ktor.client.request.setBody | |
| import io.ktor.http.ContentType | ||
| import io.ktor.http.HttpStatusCode | ||
| import io.ktor.http.contentType | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.Dispatchers | ||
| import kotlinx.coroutines.SupervisorJob | ||
| import kotlinx.coroutines.cancel | ||
| import kotlinx.coroutines.flow.StateFlow | ||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||
| import kotlinx.coroutines.flow.drop | ||
| import kotlinx.coroutines.flow.launchIn | ||
| import kotlinx.coroutines.flow.onEach | ||
| import kotlinx.coroutines.sync.Mutex | ||
| import kotlinx.coroutines.sync.withLock | ||
| import zed.rainxch.core.data.dto.BackendExploreResponse | ||
| import zed.rainxch.core.data.dto.BackendRepoResponse | ||
| import zed.rainxch.core.data.dto.BackendSearchResponse | ||
| import zed.rainxch.core.data.dto.EventRequest | ||
| import zed.rainxch.core.domain.model.ProxyConfig | ||
| import kotlin.coroutines.cancellation.CancellationException | ||
|
|
||
| class BackendApiClient { | ||
| /** | ||
| * Client for GitHub Store's own backend (trending/popular/search). | ||
| * Treated as *discovery* traffic — routes through the discovery-scope | ||
| * proxy so users configuring a proxy for GitHub browsing also have | ||
| * their backend discovery requests proxied consistently. | ||
| */ | ||
| class BackendApiClient( | ||
| proxyConfigFlow: StateFlow<ProxyConfig>, | ||
| ) { | ||
| private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) | ||
| private val mutex = Mutex() | ||
|
|
||
| private val httpClient = HttpClient { | ||
| install(ContentNegotiation) { | ||
| json(Json { ignoreUnknownKeys = true }) | ||
| } | ||
| install(HttpTimeout) { | ||
| requestTimeoutMillis = 5_000 | ||
| connectTimeoutMillis = 3_000 | ||
| socketTimeoutMillis = 5_000 | ||
| } | ||
| defaultRequest { | ||
| url(BASE_URL) | ||
| @Volatile | ||
| private var httpClient: HttpClient = buildClient(proxyConfigFlow.value) | ||
|
|
||
| init { | ||
| proxyConfigFlow | ||
| .drop(1) | ||
| .distinctUntilChanged() | ||
| .onEach { config -> | ||
| mutex.withLock { | ||
| httpClient.close() | ||
| httpClient = buildClient(config) | ||
| } | ||
| }.launchIn(scope) | ||
| } | ||
|
|
||
| private fun buildClient(proxyConfig: ProxyConfig): HttpClient = | ||
| createPlatformHttpClient(proxyConfig).config { | ||
| install(ContentNegotiation) { | ||
| json(Json { ignoreUnknownKeys = true }) | ||
| } | ||
| install(HttpTimeout) { | ||
| requestTimeoutMillis = 5_000 | ||
| connectTimeoutMillis = 3_000 | ||
| socketTimeoutMillis = 5_000 | ||
| } | ||
| defaultRequest { | ||
| url(BASE_URL) | ||
| } | ||
| expectSuccess = false | ||
| } | ||
| expectSuccess = false | ||
|
|
||
| fun close() { | ||
| httpClient.close() | ||
| scope.cancel() | ||
| } | ||
|
Comment on lines
+80
to
83
Contributor
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.
🔒 Proposed fix in SharedModule.kt- single<BackendApiClient> {
- BackendApiClient(
- proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY),
- )
- }
+ single<BackendApiClient> {
+ BackendApiClient(
+ proxyConfigFlow = ProxyManager.configFlow(ProxyScope.DISCOVERY),
+ )
+ } onClose { it?.close() }Same treatment would be appropriate for the 🤖 Prompt for AI Agents |
||
|
|
||
| suspend fun getCategory(category: String, platform: String): Result<List<BackendRepoResponse>> = | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -42,15 +42,20 @@ class GitHubClientProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .distinctUntilChanged() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .onEach { proxyConfig -> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mutex.withLock { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentClient.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentClient = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Replace-then-close: readers of [client] always see | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // a live client. Closing first opens a window where | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // an in-flight call could touch a closed engine. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val replacement = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| createGitHubHttpClient( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tokenStore = tokenStore, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rateLimitRepository = rateLimitRepository, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| authenticationState = authenticationState, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope = scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| proxyConfig = proxyConfig, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val previous = currentClient | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentClient = replacement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| previous.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+45
to
+58
Contributor
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. 🧩 Analysis chain🏁 Script executed: # Find and read the GitHubClientProvider.kt file
find . -name "GitHubClientProvider.kt" -type fRepository: OpenHub-Store/GitHub-Store Length of output: 157 🏁 Script executed: # Also search for TranslationClientProvider.kt
find . -name "TranslationClientProvider.kt" -type fRepository: OpenHub-Store/GitHub-Store Length of output: 162 🏁 Script executed: # Check the structure of the core/data directory
fd --type f --extension kt "core/data" | head -20Repository: OpenHub-Store/GitHub-Store Length of output: 411 🏁 Script executed: # Read the GitHubClientProvider.kt file to verify the code snippet
cat -n ./core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/GitHubClientProvider.ktRepository: OpenHub-Store/GitHub-Store Length of output: 3056 🏁 Script executed: # Read the TranslationClientProvider.kt file to check for similar pattern
cat -n ./core/data/src/commonMain/kotlin/zed/rainxch/core/data/network/TranslationClientProvider.ktRepository: OpenHub-Store/GitHub-Store Length of output: 2553 Delay disposal of The Mutex protects the client swap itself, but a thread can read This same race exists in 🛡️ Possible drain-period fix+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@
class GitHubClientProvider(
@@
) {
+ private companion object {
+ const val CLIENT_REPLACEMENT_DRAIN_MS = 30_000L
+ }
+
@@
val previous = currentClient
currentClient = replacement
- previous.close()
+ scope.launch {
+ try {
+ delay(CLIENT_REPLACEMENT_DRAIN_MS)
+ } finally {
+ previous.close()
+ }
+ }
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }.launchIn(scope) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| package zed.rainxch.core.data.network | ||
|
|
||
| /** | ||
| * Marker type that represents "the [ProxyManager] has been seeded | ||
| * with the user's persisted per-scope proxy configs." Any component | ||
| * that constructs an HTTP client whose proxy is read from | ||
| * [ProxyManager] at creation time must inject this so Koin resolves | ||
| * the seeding step first — it forces the DI graph dependency to be | ||
| * explicit instead of depending on registration order. | ||
| */ | ||
| class ProxyManagerSeeding internal constructor() |
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.
🧩 Analysis chain
🏁 Script executed:
Repository: OpenHub-Store/GitHub-Store
Length of output: 2077
🏁 Script executed:
# Check the package declaration and overall file structure head -20 core/data/src/commonMain/kotlin/zed/rainxch/core/data/di/SharedModule.ktRepository: OpenHub-Store/GitHub-Store
Length of output: 917
🏁 Script executed:
Repository: OpenHub-Store/GitHub-Store
Length of output: 669
🏁 Script executed:
Repository: OpenHub-Store/GitHub-Store
Length of output: 939
🏁 Script executed:
Repository: OpenHub-Store/GitHub-Store
Length of output: 1894
🏁 Script executed:
Repository: OpenHub-Store/GitHub-Store
Length of output: 2207
Isolate seeding failures per proxy scope.
One failed
getProxyConfig(scope).first()causesawaitAll()to throw, therunCatchingcatches it, andonSuccessnever executes—leaving all scopes unconfigured (not even the in-memory defaults are applied). This contradicts the comment's intent to fall back to System proxy on failure. Failures should be caught per async task before awaiting so successful scopes are still applied.🛡️ Proposed fix to preserve successful scope reads
runBlocking { runCatching { withTimeout(1_500L) { coroutineScope { ProxyScope.entries .map { scope -> async { - scope to repository.getProxyConfig(scope).first() + val config = + runCatching { + repository.getProxyConfig(scope).first() + }.getOrDefault(ProxyConfig.System) + + scope to config } }.awaitAll() } } }.onSuccess { results ->🤖 Prompt for AI Agents