Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98396c6bcd | ||
|
|
403a883e0b | ||
|
|
53c749aaea | ||
|
|
addc31c7c1 | ||
|
|
25978bcd8f | ||
|
|
117eaad9ff | ||
|
|
91e2c369f5 |
No files matched your search
@@ -22,5 +22,5 @@ jobs:
|
||||
node-version-file: .nvmrc
|
||||
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
|
||||
- uses: cargo-bins/cargo-binstall@dc19f1e48450eefe5a29b8da6c6b00a87d730b37 # v1.18.1
|
||||
- run: cargo binstall --no-confirm cargo-shear
|
||||
- run: cargo binstall --no-confirm cargo-shear@1.13.4
|
||||
- run: cargo shear
|
||||
Generated
+2
-2
@@ -6841,7 +6841,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus"
|
||||
version = "1.0.0-beta15"
|
||||
version = "1.0.2-beta4"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"async-minecraft-ping",
|
||||
@@ -6945,7 +6945,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "theseus_gui"
|
||||
version = "1.0.0-beta15"
|
||||
version = "1.0.2-beta4"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"async_zip",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@modrinth/app-frontend",
|
||||
"private": true,
|
||||
"version": "1.0.0-beta15",
|
||||
"version": "1.0.2-beta4",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -32,6 +32,33 @@ export const ANNOUNCEMENT_CHANGE_TYPES: readonly AnnouncementChangeType[] = [
|
||||
]
|
||||
|
||||
export const launcherAnnouncements: readonly LauncherAnnouncement[] = [
|
||||
{
|
||||
id: 'launcher-1.0.2-beta4',
|
||||
version: '1.0.2-beta4',
|
||||
publishedAt: '2026-10-08',
|
||||
title: {
|
||||
'en-US': 'Starlight Launcher 1.0.2-beta4',
|
||||
'zh-CN': 'Starlight Launcher 1.0.2-beta4',
|
||||
},
|
||||
changes: {
|
||||
changed: [
|
||||
{
|
||||
'en-US': 'Improved download scheduling to reduce concurrency automatically when tasks are waiting for capacity, making large Minecraft installations more reliable on constrained connections.',
|
||||
'zh-CN': '优化下载任务调度:任务因容量不足排队时会自动降低并发,提升大型 Minecraft 版本在受限网络环境下的安装稳定性。',
|
||||
},
|
||||
],
|
||||
fixed: [
|
||||
{
|
||||
'en-US': 'Fixed downloads timing out while waiting for internal connection capacity, which could cause Minecraft installations to fail even when the network was available.',
|
||||
'zh-CN': '修复下载任务等待内部连接容量时过早超时,导致网络正常的情况下 Minecraft 安装仍可能失败的问题。',
|
||||
},
|
||||
{
|
||||
'en-US': 'Fixed instance group changes not saving for some instances.',
|
||||
'zh-CN': '修复部分实例的分组修改无法保存的问题。',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'launcher-1.0.2-beta3',
|
||||
version: '1.0.2-beta3',
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
# The actual version is set by the theseus-build workflow on tagging
|
||||
version = "1.0.0-beta15"
|
||||
version = "1.0.2-beta4"
|
||||
edition.workspace = true
|
||||
description = "Axolotl Launcher is a cross-platform Minecraft launcher"
|
||||
homepage = "https://www.ghs.red"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "theseus"
|
||||
# The actual version is set by the theseus-build workflow on tagging
|
||||
version = "1.0.0-beta15"
|
||||
version = "1.0.2-beta4"
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -19,7 +19,7 @@ use uuid::Uuid;
|
||||
|
||||
use crate::{ErrorKind, State};
|
||||
|
||||
const CATALOG_SOURCE: &str = "LobeHub 906b10e03029648655e0257bda4f785a9e0973f0";
|
||||
const CATALOG_SOURCE: &str = "LobeHub 6c7a6fb033e443bc27bf34cbd5509ab3db7f22f3";
|
||||
const KEYRING_PREFIX: &str = "ai-provider";
|
||||
const OAUTH_CREDENTIAL: &str = "oauth";
|
||||
const API_KEY_CREDENTIAL: &str = "api-key";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"source": "LobeHub 906b10e03029648655e0257bda4f785a9e0973f0 model-bank chat models",
|
||||
"source": "LobeHub 6c7a6fb033e443bc27bf34cbd5509ab3db7f22f3 model-bank chat models",
|
||||
"providers": {
|
||||
"ai21": [
|
||||
{
|
||||
@@ -203,6 +203,16 @@
|
||||
}
|
||||
],
|
||||
"aihubmix": [
|
||||
{
|
||||
"id": "gpt-6-sol",
|
||||
"name": "GPT-6 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-luna",
|
||||
"name": "GPT-6 Luna",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.5",
|
||||
"name": "GPT-5.5",
|
||||
@@ -343,6 +353,11 @@
|
||||
"name": "ChatGPT-4o",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "grok-4.7",
|
||||
"name": "Grok 4.7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "grok-4-20-non-reasoning",
|
||||
"name": "Grok 4.20 (Non-Reasoning)",
|
||||
@@ -368,6 +383,11 @@
|
||||
"name": "Grok 3 Mini",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-5-5",
|
||||
"name": "Claude Opus 5.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-7",
|
||||
"name": "Claude Opus 4.7",
|
||||
@@ -383,11 +403,6 @@
|
||||
"name": "Claude Opus 4.5",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-1-20250805",
|
||||
"name": "Claude Opus 4.1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "claude-sonnet-4-6",
|
||||
"name": "Claude Sonnet 4.6",
|
||||
@@ -403,6 +418,21 @@
|
||||
"name": "Claude Haiku 4.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.6-pro",
|
||||
"name": "MiMo-V2.6 Pro",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.6-pro-ultraspeed",
|
||||
"name": "MiMo-V2.6 Pro UltraSpeed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.6-flash",
|
||||
"name": "MiMo-V2.6 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "DeepSeek-V3.1-Think",
|
||||
"name": "DeepSeek V3.1 (Think)",
|
||||
@@ -542,11 +572,6 @@
|
||||
"name": "Ring-2.6-1T",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Ring-2.5-1T",
|
||||
"name": "Ring-2.5-1T",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Ling-3.0-flash",
|
||||
"name": "Ling-3.0-flash",
|
||||
@@ -561,14 +586,14 @@
|
||||
"id": "Ling-2.6-flash",
|
||||
"name": "Ling-2.6-flash",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Ling-2.5-1T",
|
||||
"name": "Ling-2.5-1T",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"anthropic": [
|
||||
{
|
||||
"id": "claude-opus-5-5",
|
||||
"name": "Claude Opus 5.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "claude-fable-5-1",
|
||||
"name": "Claude Fable 5.1",
|
||||
@@ -623,14 +648,24 @@
|
||||
"id": "claude-haiku-4-5-20251001",
|
||||
"name": "Claude Haiku 4.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "claude-opus-4-1-20250805",
|
||||
"name": "Claude Opus 4.1",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"azure": [
|
||||
{
|
||||
"id": "gpt-6-astra",
|
||||
"name": "GPT-6 Astra",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-sol",
|
||||
"name": "GPT-6 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-luna",
|
||||
"name": "GPT-6 Luna",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.4",
|
||||
"name": "GPT-5.4",
|
||||
@@ -914,6 +949,11 @@
|
||||
}
|
||||
],
|
||||
"bedrock": [
|
||||
{
|
||||
"id": "global.anthropic.claude-opus-5-5",
|
||||
"name": "Claude Opus 5.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "global.anthropic.claude-opus-4-7",
|
||||
"name": "Claude Opus 4.7",
|
||||
@@ -1030,14 +1070,29 @@
|
||||
"id": "gpt-oss-120b",
|
||||
"name": "GPT OSS 120B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "zai-glm-4.7",
|
||||
"name": "GLM-4.7",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"chatgpt": [
|
||||
{
|
||||
"id": "gpt-6.1-sol",
|
||||
"name": "GPT-6.1 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-astra",
|
||||
"name": "GPT-6 Astra",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-sol",
|
||||
"name": "GPT-6 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-luna",
|
||||
"name": "GPT-6 Luna",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.6-sol",
|
||||
"name": "GPT-5.6 Sol",
|
||||
@@ -1056,7 +1111,7 @@
|
||||
{
|
||||
"id": "gpt-5.5",
|
||||
"name": "GPT-5.5",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"cloudflare": [
|
||||
@@ -1883,10 +1938,15 @@
|
||||
"name": "Gemini Flash-Lite Latest",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-3.8-flash",
|
||||
"name": "Gemini 3.8 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gemini-3.7-flash",
|
||||
"name": "Gemini 3.7 Flash",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-3.6-flash",
|
||||
@@ -1972,26 +2032,6 @@
|
||||
"id": "gemini-2.5-flash-lite",
|
||||
"name": "Gemini 2.5 Flash-Lite",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash",
|
||||
"name": "Gemini 2.0 Flash",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-001",
|
||||
"name": "Gemini 2.0 Flash 001",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-lite",
|
||||
"name": "Gemini 2.0 Flash-Lite",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.0-flash-lite-001",
|
||||
"name": "Gemini 2.0 Flash-Lite 001",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"groq": [
|
||||
@@ -2005,10 +2045,15 @@
|
||||
"name": "Compound Mini",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "qwen/qwen3.8-27b",
|
||||
"name": "Qwen3.8 27B",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "qwen/qwen3.6-27b",
|
||||
"name": "Qwen3.6 27B",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "minimaxai/minimax-m2.7",
|
||||
@@ -2025,16 +2070,6 @@
|
||||
"name": "GPT OSS 20B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "llama-3.1-8b-instant",
|
||||
"name": "Llama 3.1 8B Instant",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "llama-3.3-70b-versatile",
|
||||
"name": "Llama 3.3 70B Versatile",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "meta-llama/llama-prompt-guard-2-22m",
|
||||
"name": "Llama Prompt Guard 2 22M",
|
||||
@@ -3575,13 +3610,13 @@
|
||||
],
|
||||
"mistral": [
|
||||
{
|
||||
"id": "mistral-medium-3.5",
|
||||
"name": "Mistral Medium 3.5",
|
||||
"enabled": true
|
||||
"id": "zai-glm-5-2",
|
||||
"name": "Z.ai GLM 5.2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "devstral-2512",
|
||||
"name": "Devstral 2",
|
||||
"id": "mistral-medium-3.5",
|
||||
"name": "Mistral Medium 3.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -3589,61 +3624,21 @@
|
||||
"name": "Devstral Small 2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "mistral-medium-2508",
|
||||
"name": "Mistral Medium 3.1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "magistral-medium-2509",
|
||||
"name": "Magistral Medium 1.2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "magistral-small-2509",
|
||||
"name": "Magistral Small 1.2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "open-mistral-nemo",
|
||||
"name": "Mistral Nemo",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "mistral-small-2603",
|
||||
"name": "Mistral Small 4",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "mistral-small-2506",
|
||||
"name": "Mistral Small 3.2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "mistral-large-2512",
|
||||
"name": "Mistral Large 3",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mistral-large-2411",
|
||||
"name": "Mistral Large 2.1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "codestral-latest",
|
||||
"name": "Codestral 2508",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "pixtral-large-latest",
|
||||
"name": "Pixtral Large",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "pixtral-12b-2409",
|
||||
"name": "Pixtral 12B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "ministral-3b-latest",
|
||||
"name": "Ministral 3B",
|
||||
@@ -3653,16 +3648,6 @@
|
||||
"id": "ministral-8b-latest",
|
||||
"name": "Ministral 8B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "open-codestral-mamba",
|
||||
"name": "Codestral Mamba",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "labs-leanstral-2603",
|
||||
"name": "Leanstral",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"modelscope": [
|
||||
@@ -3676,30 +3661,10 @@
|
||||
"name": "Qwen3 Next 80B A3B Instruct",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-ai/DeepSeek-V3.2",
|
||||
"name": "DeepSeek V3.2",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "deepseek-ai/DeepSeek-V3.2-Exp",
|
||||
"name": "DeepSeek V3.2 Exp",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-ai/DeepSeek-V3.1",
|
||||
"name": "DeepSeek V3.1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Qwen/Qwen3-235B-A22B",
|
||||
"name": "Qwen3 235B A22B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Qwen/Qwen3-32B",
|
||||
"name": "Qwen3 32B",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"moonshot": [
|
||||
@@ -3722,41 +3687,6 @@
|
||||
"id": "kimi-k2.6",
|
||||
"name": "Kimi K2.6",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "kimi-k2.5",
|
||||
"name": "Kimi K2.5",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-8k",
|
||||
"name": "Moonshot V1 8K",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-32k",
|
||||
"name": "Moonshot V1 32K",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-128k",
|
||||
"name": "Moonshot V1 128K",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-8k-vision-preview",
|
||||
"name": "Moonshot V1 8K Vision Preview",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-32k-vision-preview",
|
||||
"name": "Moonshot V1 32K Vision Preview",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "moonshot-v1-128k-vision-preview",
|
||||
"name": "Moonshot V1 128K Vision Preview",
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"nebius": [
|
||||
@@ -4424,16 +4354,6 @@
|
||||
"name": "Qwen3.5 397B A17B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "minimax-m2.5",
|
||||
"name": "MiniMax M2.5",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "kimi-k2.5",
|
||||
"name": "Kimi K2.5",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-oss:20b",
|
||||
"name": "GPT-OSS 20B",
|
||||
@@ -4451,6 +4371,26 @@
|
||||
}
|
||||
],
|
||||
"openai": [
|
||||
{
|
||||
"id": "gpt-6.1-sol",
|
||||
"name": "GPT-6.1 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-astra",
|
||||
"name": "GPT-6 Astra",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-sol",
|
||||
"name": "GPT-6 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gpt-6-luna",
|
||||
"name": "GPT-6 Luna",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "chat-latest",
|
||||
"name": "Chat Latest",
|
||||
@@ -4474,7 +4414,7 @@
|
||||
{
|
||||
"id": "gpt-5.5",
|
||||
"name": "GPT-5.5",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.5-pro",
|
||||
@@ -4484,7 +4424,7 @@
|
||||
{
|
||||
"id": "gpt-5.4",
|
||||
"name": "GPT-5.4",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.4-pro",
|
||||
@@ -4494,7 +4434,7 @@
|
||||
{
|
||||
"id": "gpt-5.4-mini",
|
||||
"name": "GPT-5.4 mini",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gpt-5.4-nano",
|
||||
@@ -4708,6 +4648,16 @@
|
||||
"name": "MiniMax M3",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "muse-spark-1.2-contributor",
|
||||
"name": "Muse Spark 1.2 Contributor",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "muse-spark-1.3-contributor",
|
||||
"name": "Muse Spark 1.3 Contributor",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "qwen3.6-plus",
|
||||
"name": "Qwen3.6 Plus",
|
||||
@@ -4912,6 +4862,41 @@
|
||||
"name": "Auto (best for prompt)",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "anthropic/claude-opus-5.5",
|
||||
"name": "Claude Opus 5.5",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-6-sol",
|
||||
"name": "GPT-6 Sol",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "openai/gpt-6-luna",
|
||||
"name": "GPT-6 Luna",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "x-ai/grok-4.7",
|
||||
"name": "Grok 4.7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "xiaomi/mimo-v2.6-pro",
|
||||
"name": "MiMo-V2.6 Pro",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "xiaomi/mimo-v2.6-pro-ultraspeed",
|
||||
"name": "MiMo-V2.6 Pro UltraSpeed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "xiaomi/mimo-v2.6-flash",
|
||||
"name": "MiMo-V2.6 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "deepseek/deepseek-chat-v3.1",
|
||||
"name": "DeepSeek V3.1",
|
||||
@@ -5260,6 +5245,26 @@
|
||||
}
|
||||
],
|
||||
"qwen": [
|
||||
{
|
||||
"id": "deepseek-v4-pro-0813",
|
||||
"name": "DeepSeek V4 Pro 0813",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-v4-flash-0731",
|
||||
"name": "DeepSeek V4 Flash 0731",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "qwen3.8-max-0902",
|
||||
"name": "Qwen3.8 Max 0902",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "qwen3.8-max",
|
||||
"name": "Qwen3.8 Max",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "kimi-k2.6",
|
||||
"name": "Kimi K2.6",
|
||||
@@ -5490,10 +5495,15 @@
|
||||
"name": "QwQ Plus",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "qwen3.8-flash",
|
||||
"name": "Qwen3.8 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "qwen3.7-flash",
|
||||
"name": "Qwen3.7 Flash",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "qwen3.6-flash",
|
||||
@@ -5802,75 +5812,40 @@
|
||||
}
|
||||
],
|
||||
"sambanova": [
|
||||
{
|
||||
"id": "MiniMax-M2.7",
|
||||
"name": "MiniMax M2.7",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "DeepSeek-V3.1",
|
||||
"name": "DeepSeek V3.1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.3-70B-Instruct",
|
||||
"name": "Meta Llama 3.3 70B Instruct",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.2-1B-Instruct",
|
||||
"name": "Meta Llama 3.2 1B Instruct",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.2-3B-Instruct",
|
||||
"name": "Meta Llama 3.2 3B Instruct",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Llama-3.2-11B-Vision-Instruct",
|
||||
"name": "Meta Llama 3.2 11B Vision Instruct",
|
||||
"id": "gpt-oss-120b",
|
||||
"name": "GPT OSS 120B",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Llama-3.2-90B-Vision-Instruct",
|
||||
"name": "Meta Llama 3.2 90B Vision Instruct",
|
||||
"id": "MiniMax-M3",
|
||||
"name": "MiniMax M3",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.1-8B-Instruct",
|
||||
"name": "Meta Llama 3.1 8B Instruct",
|
||||
"id": "DeepSeek-V3.2",
|
||||
"name": "DeepSeek V3.2",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.1-70B-Instruct",
|
||||
"name": "Meta Llama 3.1 70B Instruct",
|
||||
"id": "gemma-4-31B-it",
|
||||
"name": "Gemma 4 31B IT",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Meta-Llama-3.1-405B-Instruct",
|
||||
"name": "Meta Llama 3.1 405B Instruct",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "Llama-3.1-Tulu-3-405B",
|
||||
"name": "Llama 3.1 Tulu 3 405B",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "DeepSeek-R1",
|
||||
"name": "DeepSeek R1",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "DeepSeek-R1-Distill-Llama-70B",
|
||||
"name": "DeepSeek R1 Distill Llama 70B",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "QwQ-32B-Preview",
|
||||
"name": "QwQ 32B Preview",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Qwen2.5-72B-Instruct",
|
||||
"name": "Qwen2.5 72B Instruct",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "Qwen2.5-Coder-32B-Instruct",
|
||||
"name": "Qwen2.5 Coder 32B Instruct",
|
||||
"enabled": true
|
||||
}
|
||||
],
|
||||
"search1api": [
|
||||
@@ -6262,6 +6237,11 @@
|
||||
}
|
||||
],
|
||||
"stepfun": [
|
||||
{
|
||||
"id": "step-5-preview",
|
||||
"name": "Step 5 Preview",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "step-3.7-flash",
|
||||
"name": "Step 3.7 Flash",
|
||||
@@ -6407,6 +6387,16 @@
|
||||
}
|
||||
],
|
||||
"supergrok": [
|
||||
{
|
||||
"id": "grok-4.7",
|
||||
"name": "Grok 4.7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "grok-4.6",
|
||||
"name": "Grok 4.6",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "grok-4.5",
|
||||
"name": "Grok 4.5",
|
||||
@@ -7166,10 +7156,15 @@
|
||||
}
|
||||
],
|
||||
"vertexai": [
|
||||
{
|
||||
"id": "gemini-3.8-flash",
|
||||
"name": "Gemini 3.8 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "gemini-3.7-flash",
|
||||
"name": "Gemini 3.7 Flash",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "gemini-3.6-flash",
|
||||
@@ -7345,20 +7340,30 @@
|
||||
"name": "Doubao Seed 2.1 Turbo",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "deepseek-v4-pro-ga",
|
||||
"name": "DeepSeek V4 Pro 0813",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-v4-flash-ga",
|
||||
"name": "DeepSeek V4 Flash 0731",
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "glm-5-2",
|
||||
"name": "GLM-5.2",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-v4-pro",
|
||||
"name": "DeepSeek V4 Pro",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "deepseek-v4-flash",
|
||||
"name": "DeepSeek V4 Flash",
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"id": "doubao-seed-2.0-pro",
|
||||
@@ -7701,6 +7706,16 @@
|
||||
}
|
||||
],
|
||||
"xai": [
|
||||
{
|
||||
"id": "grok-4.7",
|
||||
"name": "Grok 4.7",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "grok-4.6",
|
||||
"name": "Grok 4.6",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "grok-4.5",
|
||||
"name": "Grok 4.5",
|
||||
@@ -7728,6 +7743,21 @@
|
||||
}
|
||||
],
|
||||
"xiaomimimo": [
|
||||
{
|
||||
"id": "mimo-v2.6-pro",
|
||||
"name": "MiMo-V2.6 Pro",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.6-pro-ultraspeed",
|
||||
"name": "MiMo-V2.6 Pro UltraSpeed",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.6-flash",
|
||||
"name": "MiMo-V2.6 Flash",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"id": "mimo-v2.5-pro",
|
||||
"name": "MiMo-V2.5 Pro",
|
||||
|
||||
@@ -1947,9 +1947,9 @@ pub async fn download_assets(
|
||||
// failures. Runs concurrently (same budget as the original scheduler) so
|
||||
// import flows are not serialised.
|
||||
if !fallback_assets.is_empty() {
|
||||
let limit = crate::util::download::task_concurrency_limit(st)
|
||||
.map(|limit| limit.saturating_mul(2))
|
||||
.unwrap_or(ASSET_BATCH_CONCURRENCY);
|
||||
let limit =
|
||||
crate::util::download::single_authority_task_concurrency_limit(st)
|
||||
.unwrap_or(ASSET_BATCH_CONCURRENCY);
|
||||
futures::stream::iter(fallback_assets)
|
||||
.map(Ok::<FallbackAsset, crate::Error>)
|
||||
.try_for_each_concurrent(limit, |item| {
|
||||
@@ -2123,7 +2123,7 @@ pub async fn download_libraries(
|
||||
let num_files = tasks.len();
|
||||
loading_try_for_each_concurrent(
|
||||
stream::iter(tasks).map(Ok::<LibraryDownloadTask<'_>, crate::Error>),
|
||||
crate::util::download::task_concurrency_limit(&st).map(|limit| limit.saturating_mul(2)),
|
||||
crate::util::download::task_concurrency_limit(&st),
|
||||
loading_bar,
|
||||
loading_amount,
|
||||
num_files,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::api::pack::import::{
|
||||
ImportLauncherType,
|
||||
direct_link::resolve_direct_link,
|
||||
ImportLauncherType, direct_link::resolve_direct_link,
|
||||
};
|
||||
use crate::launcher::ExternalGameDirMode;
|
||||
use crate::state::instances::{
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::create_direct_link_instance::create_direct_link_instance;
|
||||
use crate::api::pack::import::direct_link::{
|
||||
detect_direct_link_source,
|
||||
has_minecraft_version_manifest, resolve_direct_link,
|
||||
detect_direct_link_source, has_minecraft_version_manifest,
|
||||
resolve_direct_link,
|
||||
};
|
||||
use crate::event::{InstancePayloadType, emit::emit_instance};
|
||||
use crate::launcher::ExternalGameDirMode;
|
||||
|
||||
@@ -118,6 +118,7 @@ pub struct State {
|
||||
download_sample_requests: AtomicU64,
|
||||
download_sample_errors: AtomicU64,
|
||||
download_sample_throttles: AtomicU64,
|
||||
download_sample_queue_waits: AtomicU64,
|
||||
pub(crate) install_job_semaphore: Semaphore,
|
||||
pub(crate) install_db_semaphore: Semaphore,
|
||||
pub(crate) install_job_cancellations: DashMap<Uuid, CancellationToken>,
|
||||
@@ -196,6 +197,7 @@ struct AutoConcurrencySample {
|
||||
requests: u64,
|
||||
errors: u64,
|
||||
throttles: u64,
|
||||
queue_waits: u64,
|
||||
}
|
||||
|
||||
/// Per-instance lock registry with task-local reentrancy.
|
||||
@@ -399,6 +401,15 @@ impl AutoConcurrencyController {
|
||||
} else {
|
||||
sample.errors as f64 / sample.requests as f64
|
||||
};
|
||||
if sample.queue_waits > 0 {
|
||||
self.high_utilization_windows = 0;
|
||||
self.pressure_windows = 0;
|
||||
self.probe = None;
|
||||
self.cooldown_until = Some(now + AUTO_DOWNLOAD_PROBE_COOLDOWN);
|
||||
return current
|
||||
.saturating_sub(current.div_ceil(4))
|
||||
.max(AUTO_DOWNLOAD_CONCURRENCY_MIN);
|
||||
}
|
||||
// A single throttled window is handled by the per-host limiter; the
|
||||
// global concurrency only backs off when pressure persists across
|
||||
// several samples, and then in small steps instead of a big cut.
|
||||
@@ -677,6 +688,11 @@ impl State {
|
||||
self.download_sample_errors.fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
|
||||
pub(crate) fn record_download_queue_wait(&self) {
|
||||
self.download_sample_queue_waits
|
||||
.fetch_add(1, Ordering::AcqRel);
|
||||
}
|
||||
|
||||
pub(crate) fn update_download_settings(
|
||||
self: &Arc<Self>,
|
||||
settings: &Settings,
|
||||
@@ -735,6 +751,7 @@ impl State {
|
||||
self.download_sample_requests.swap(0, Ordering::AcqRel);
|
||||
self.download_sample_errors.swap(0, Ordering::AcqRel);
|
||||
self.download_sample_throttles.swap(0, Ordering::AcqRel);
|
||||
self.download_sample_queue_waits.swap(0, Ordering::AcqRel);
|
||||
continue;
|
||||
}
|
||||
let sample = AutoConcurrencySample {
|
||||
@@ -749,6 +766,9 @@ impl State {
|
||||
throttles: self
|
||||
.download_sample_throttles
|
||||
.swap(0, Ordering::AcqRel),
|
||||
queue_waits: self
|
||||
.download_sample_queue_waits
|
||||
.swap(0, Ordering::AcqRel),
|
||||
};
|
||||
let current = self.download_concurrency();
|
||||
let ceiling =
|
||||
@@ -771,6 +791,7 @@ impl State {
|
||||
requests = sample.requests,
|
||||
errors = sample.errors,
|
||||
throttles = sample.throttles,
|
||||
queue_waits = sample.queue_waits,
|
||||
"Adjusted automatic download concurrency"
|
||||
);
|
||||
self.resize_download_concurrency(target);
|
||||
@@ -935,6 +956,7 @@ impl State {
|
||||
download_sample_requests: AtomicU64::new(0),
|
||||
download_sample_errors: AtomicU64::new(0),
|
||||
download_sample_throttles: AtomicU64::new(0),
|
||||
download_sample_queue_waits: AtomicU64::new(0),
|
||||
install_job_semaphore: Semaphore::new(MAX_CONCURRENT_INSTALL_JOBS),
|
||||
install_db_semaphore: Semaphore::new(1),
|
||||
install_job_cancellations: DashMap::new(),
|
||||
@@ -1024,6 +1046,7 @@ pub(crate) async fn test_state(
|
||||
download_sample_requests: AtomicU64::new(0),
|
||||
download_sample_errors: AtomicU64::new(0),
|
||||
download_sample_throttles: AtomicU64::new(0),
|
||||
download_sample_queue_waits: AtomicU64::new(0),
|
||||
install_job_semaphore: Semaphore::new(1),
|
||||
install_db_semaphore: Semaphore::new(1),
|
||||
install_job_cancellations: DashMap::new(),
|
||||
@@ -1053,9 +1076,24 @@ mod auto_concurrency_tests {
|
||||
requests: 100,
|
||||
errors: 0,
|
||||
throttles: 0,
|
||||
queue_waits: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queue_pressure_backs_off_immediately_and_repeatedly() {
|
||||
let mut controller = AutoConcurrencyController::default();
|
||||
let now = Instant::now();
|
||||
let congested = AutoConcurrencySample {
|
||||
queue_waits: 1,
|
||||
..healthy(8, 100)
|
||||
};
|
||||
|
||||
assert_eq!(controller.next_target(now, 128, 128, congested), 96);
|
||||
assert_eq!(controller.next_target(now, 96, 128, congested), 72);
|
||||
assert_eq!(controller.next_target(now, 16, 128, congested), 16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probes_up_after_two_saturated_windows_and_respects_ceiling() {
|
||||
let mut controller = AutoConcurrencyController::default();
|
||||
@@ -1130,6 +1168,7 @@ mod auto_concurrency_tests {
|
||||
requests: 100,
|
||||
errors: 5,
|
||||
throttles: 0,
|
||||
queue_waits: 0,
|
||||
};
|
||||
assert_eq!(controller.next_target(Instant::now(), 16, 128, sample), 16);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ pub(crate) enum H2DownloadOutcome {
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum H2DownloadFailure {
|
||||
Ineligible(&'static str),
|
||||
Capacity,
|
||||
Connect,
|
||||
Tls,
|
||||
Protocol,
|
||||
@@ -76,6 +77,7 @@ impl H2DownloadFailure {
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Ineligible(reason) => reason,
|
||||
Self::Capacity => "HTTP/2 connection capacity is unavailable",
|
||||
Self::Connect => "HTTP/2 TCP connection failed",
|
||||
Self::Tls => "HTTP/2 TLS connection failed",
|
||||
Self::Protocol => "HTTP/2 protocol failed",
|
||||
@@ -104,6 +106,7 @@ impl H2DownloadFailure {
|
||||
pub(crate) async fn try_download_via_h2(
|
||||
request: &DownloadRequest,
|
||||
route: &DownloadRoute,
|
||||
semaphore: &fetch::FetchSemaphore,
|
||||
destination: &Path,
|
||||
part_path: &Path,
|
||||
policy: super::native::NativeH2Policy,
|
||||
@@ -122,13 +125,19 @@ pub(crate) async fn try_download_via_h2(
|
||||
preserve_partial: false,
|
||||
};
|
||||
}
|
||||
let connection = match connect_authority(
|
||||
route,
|
||||
true,
|
||||
policy.allow_cold_connection,
|
||||
)
|
||||
.await
|
||||
let connection_wait =
|
||||
connect_authority(route, true, policy.allow_cold_connection);
|
||||
let connection_result = if let Some(cancellation) =
|
||||
request.cancellation.as_ref()
|
||||
{
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => return H2DownloadOutcome::Canceled,
|
||||
result = connection_wait => result,
|
||||
}
|
||||
} else {
|
||||
connection_wait.await
|
||||
};
|
||||
let connection = match connection_result {
|
||||
Ok(connection) => connection,
|
||||
Err(failure) => {
|
||||
return H2DownloadOutcome::Fallback {
|
||||
@@ -137,6 +146,30 @@ pub(crate) async fn try_download_via_h2(
|
||||
};
|
||||
}
|
||||
};
|
||||
// The shared connection reserves native capacity when it is created.
|
||||
// Acquire the per-download permit afterwards so every native path uses
|
||||
// the same native-then-fetch order and cannot form a circular wait.
|
||||
let fetch_permit_wait =
|
||||
fetch::acquire_fetch_permit_with_pressure(semaphore);
|
||||
let fetch_permit_result = if let Some(cancellation) =
|
||||
request.cancellation.as_ref()
|
||||
{
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => return H2DownloadOutcome::Canceled,
|
||||
result = fetch_permit_wait => result,
|
||||
}
|
||||
} else {
|
||||
fetch_permit_wait.await
|
||||
};
|
||||
let _fetch_permit = match fetch_permit_result {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => {
|
||||
return H2DownloadOutcome::Fallback {
|
||||
failure: H2DownloadFailure::Connect,
|
||||
preserve_partial: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
let Ok(uri) = route.url.parse::<Uri>() else {
|
||||
return H2DownloadOutcome::Fallback {
|
||||
failure: H2DownloadFailure::Http,
|
||||
@@ -156,14 +189,20 @@ pub(crate) async fn try_download_via_h2(
|
||||
let total_size = if let Some(size) = expected_size {
|
||||
size
|
||||
} else {
|
||||
let _probe_stream_permit = match tokio::time::timeout(
|
||||
ASSET_RESOURCE_WAIT_TIMEOUT,
|
||||
super::h2_stream_budget::acquire(route),
|
||||
)
|
||||
.await
|
||||
let probe_wait = super::h2_stream_budget::acquire_with_pressure(route);
|
||||
let probe_result = if let Some(cancellation) =
|
||||
request.cancellation.as_ref()
|
||||
{
|
||||
Ok(Ok(permit)) => permit,
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => return H2DownloadOutcome::Canceled,
|
||||
result = probe_wait => result,
|
||||
}
|
||||
} else {
|
||||
probe_wait.await
|
||||
};
|
||||
let _probe_stream_permit = match probe_result {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => {
|
||||
return H2DownloadOutcome::Fallback {
|
||||
failure: H2DownloadFailure::Connect,
|
||||
preserve_partial: false,
|
||||
@@ -239,10 +278,7 @@ pub(crate) async fn try_download_via_h2(
|
||||
crate::install::DownloadItemStatus::WaitingForResource,
|
||||
)
|
||||
.await;
|
||||
let stream_wait = tokio::time::timeout(
|
||||
ASSET_RESOURCE_WAIT_TIMEOUT,
|
||||
super::h2_stream_budget::acquire(route),
|
||||
);
|
||||
let stream_wait = super::h2_stream_budget::acquire_with_pressure(route);
|
||||
let stream_wait_started = Instant::now();
|
||||
let stream_result = if let Some(cancellation) =
|
||||
request.cancellation.as_ref()
|
||||
@@ -255,8 +291,8 @@ pub(crate) async fn try_download_via_h2(
|
||||
stream_wait.await
|
||||
};
|
||||
let _stream_permit = match stream_result {
|
||||
Ok(Ok(permit)) => permit,
|
||||
Ok(Err(_)) | Err(_) => {
|
||||
Ok(permit) => permit,
|
||||
Err(_) => {
|
||||
return H2DownloadOutcome::Fallback {
|
||||
failure: H2DownloadFailure::Connect,
|
||||
preserve_partial: false,
|
||||
@@ -327,6 +363,7 @@ async fn connect_authority(
|
||||
"Failed to establish shared HTTP/2 connection"
|
||||
);
|
||||
Err(match error.kind {
|
||||
H2ConnectFailureKind::Capacity => H2DownloadFailure::Capacity,
|
||||
H2ConnectFailureKind::Tcp => H2DownloadFailure::Connect,
|
||||
H2ConnectFailureKind::Tls => H2DownloadFailure::Tls,
|
||||
H2ConnectFailureKind::Protocol => H2DownloadFailure::Protocol,
|
||||
@@ -771,6 +808,12 @@ mod tests {
|
||||
use super::*;
|
||||
use bytes::Bytes;
|
||||
|
||||
#[test]
|
||||
fn capacity_wait_is_not_classified_as_a_transport_failure() {
|
||||
assert!(!H2DownloadFailure::Capacity.is_transfer_failure());
|
||||
assert!(!H2DownloadFailure::Capacity.should_cooldown_authority());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn single_stream_reports_bytes_before_the_response_finishes() {
|
||||
let size = 512 * 1024;
|
||||
@@ -1183,28 +1226,6 @@ async fn download_asset_item(
|
||||
"timed out waiting for asset destination lock".to_string(),
|
||||
)
|
||||
})?;
|
||||
let fetch_permit = if apply_native_policy {
|
||||
let Some(semaphore) = native_semaphore else {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"native asset batch is missing fetch budget".to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
Some(
|
||||
tokio::time::timeout(
|
||||
ASSET_RESOURCE_WAIT_TIMEOUT,
|
||||
semaphore.0.acquire(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::NetworkError(
|
||||
"timed out waiting for asset fetch permit".to_string(),
|
||||
)
|
||||
})??,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A different downloader may have committed the object while this item
|
||||
// waited for the destination lock. Reuse it instead of opening another
|
||||
// stream, which also prevents cross-engine `.part`/rename races.
|
||||
@@ -1243,24 +1264,26 @@ async fn download_asset_item(
|
||||
return Ok(AssetBatchItemOutcome::LocalObjectFailed { error });
|
||||
}
|
||||
};
|
||||
let _stream_permit = if apply_native_policy {
|
||||
Some(
|
||||
tokio::time::timeout(
|
||||
ASSET_RESOURCE_WAIT_TIMEOUT,
|
||||
super::h2_stream_budget::acquire_asset(route),
|
||||
// Resolve the primary or lazily expanded sibling before taking the fetch
|
||||
// permit. Shared connections reserve native capacity, so this preserves
|
||||
// the native-then-fetch order used by every other native path.
|
||||
let connection = connections.connection(rescue).await;
|
||||
let fetch_permit = if apply_native_policy {
|
||||
let Some(semaphore) = native_semaphore else {
|
||||
return Err(crate::ErrorKind::OtherError(
|
||||
"native asset batch is missing fetch budget".to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::ErrorKind::NetworkError(
|
||||
"timed out waiting for asset HTTP/2 stream permit"
|
||||
.to_string(),
|
||||
)
|
||||
})??,
|
||||
)
|
||||
.into());
|
||||
};
|
||||
Some(fetch::acquire_fetch_permit_with_pressure(semaphore).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let _stream_permit = if apply_native_policy {
|
||||
Some(super::h2_stream_budget::acquire_asset_with_pressure(route).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let connection = connections.connection(rescue).await;
|
||||
let _connection_stream = connection.track_stream();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
|
||||
@@ -34,6 +34,7 @@ const CONNECTION_WAIT_TIMEOUT: Duration = Duration::from_secs(45);
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) enum H2ConnectFailureKind {
|
||||
Capacity,
|
||||
Tcp,
|
||||
Tls,
|
||||
Protocol,
|
||||
@@ -357,9 +358,41 @@ async fn connect_tcp(host: &str, port: u16) -> std::io::Result<TcpStream> {
|
||||
}
|
||||
|
||||
/// Connects a new shared HTTP/2 connection to `authority` (host[:port]).
|
||||
async fn establish(
|
||||
async fn reserve_physical_budget(
|
||||
route: &DownloadRoute,
|
||||
reserve_native_budget: bool,
|
||||
) -> Result<Option<super::native_budget::NativeBudgetPermit>, H2ConnectError> {
|
||||
Ok(if reserve_native_budget {
|
||||
Some(
|
||||
crate::util::fetch::wait_for_download_resource(
|
||||
super::native_budget::acquire(route),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Capacity,
|
||||
"HTTP/2 connection capacity remained stalled for 5 minutes"
|
||||
.to_string(),
|
||||
)
|
||||
})?
|
||||
.map_err(|error| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Capacity,
|
||||
format!(
|
||||
"failed to reserve HTTP/2 connection capacity: {error}"
|
||||
),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Connects a new shared HTTP/2 connection to `authority` (host[:port]).
|
||||
async fn establish(
|
||||
route: &DownloadRoute,
|
||||
physical_budget: Option<super::native_budget::NativeBudgetPermit>,
|
||||
) -> Result<Arc<SharedH2Connection>, H2ConnectError> {
|
||||
let authority =
|
||||
crate::util::fetch::url_authority(&route.url).ok_or_else(|| {
|
||||
@@ -368,16 +401,6 @@ async fn establish(
|
||||
"HTTP/2 route has no authority".to_string(),
|
||||
)
|
||||
})?;
|
||||
let physical_budget = if reserve_native_budget {
|
||||
Some(super::native_budget::acquire(route).await.map_err(|error| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Tcp,
|
||||
format!("failed to reserve HTTP/2 connection capacity: {error}"),
|
||||
)
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (host, port) = authority
|
||||
.rsplit_once(':')
|
||||
.map(|(host, port)| (host, port.parse::<u16>().unwrap_or(443)))
|
||||
@@ -487,16 +510,16 @@ pub(crate) async fn shared_connection(
|
||||
)
|
||||
})?;
|
||||
let slot = connection_slot(&authority).await;
|
||||
let mut cached = tokio::time::timeout(CONNECTION_WAIT_TIMEOUT, slot.lock())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Protocol,
|
||||
format!(
|
||||
"timed out waiting for HTTP/2 connection slot {authority}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let mut cached = crate::util::fetch::wait_for_download_resource(
|
||||
slot.lock(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Capacity,
|
||||
format!("HTTP/2 connection slot remained stalled for {authority}"),
|
||||
)
|
||||
})?;
|
||||
if let Some(connection) = cached.as_ref().filter(|connection| {
|
||||
!connection.is_dead() && !connection.is_idle_expired()
|
||||
}) {
|
||||
@@ -525,9 +548,11 @@ pub(crate) async fn shared_connection(
|
||||
));
|
||||
}
|
||||
tracing::debug!(authority, "Establishing cold shared HTTP/2 connection");
|
||||
let physical_budget =
|
||||
reserve_physical_budget(route, reserve_native_budget).await?;
|
||||
let connection = tokio::time::timeout(
|
||||
CONNECTION_WAIT_TIMEOUT,
|
||||
establish(route, reserve_native_budget),
|
||||
establish(route, physical_budget),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@@ -555,15 +580,16 @@ pub(crate) async fn shared_batch_connection(
|
||||
)
|
||||
})?;
|
||||
let slot = batch_connection_slot(&authority).await;
|
||||
let mut cached = tokio::time::timeout(
|
||||
CONNECTION_WAIT_TIMEOUT,
|
||||
let mut cached = crate::util::fetch::wait_for_download_resource(
|
||||
slot.lock(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
H2ConnectError::new(
|
||||
H2ConnectFailureKind::Protocol,
|
||||
format!("timed out waiting for asset HTTP/2 connection slot {authority}"),
|
||||
H2ConnectFailureKind::Capacity,
|
||||
format!(
|
||||
"asset HTTP/2 connection slot remained stalled for {authority}"
|
||||
),
|
||||
)
|
||||
})?;
|
||||
if let Some(connection) = cached.as_ref().filter(|connection| {
|
||||
@@ -591,9 +617,11 @@ pub(crate) async fn shared_batch_connection(
|
||||
*cached = None;
|
||||
}
|
||||
tracing::debug!(authority, "Establishing sibling HTTP/2 asset connection");
|
||||
let physical_budget =
|
||||
reserve_physical_budget(route, reserve_native_budget).await?;
|
||||
let connection = tokio::time::timeout(
|
||||
CONNECTION_WAIT_TIMEOUT,
|
||||
establish(route, reserve_native_budget),
|
||||
establish(route, physical_budget),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
@@ -614,10 +642,13 @@ pub(crate) async fn has_live_connection(authority: &str) -> bool {
|
||||
return false;
|
||||
};
|
||||
drop(connections);
|
||||
let live = slot
|
||||
.lock()
|
||||
.await
|
||||
let Ok(cached) = slot.try_lock() else {
|
||||
// This is only an eligibility probe. A busy slot means another task
|
||||
// is establishing or refreshing the connection; do not queue this
|
||||
// caller behind that work before its cancellation-aware download path.
|
||||
return false;
|
||||
};
|
||||
cached
|
||||
.as_ref()
|
||||
.is_some_and(|connection| !connection.is_dead());
|
||||
live
|
||||
.is_some_and(|connection| !connection.is_dead())
|
||||
}
|
||||
@@ -218,13 +218,9 @@ async fn download_range(
|
||||
crate::install::DownloadItemStatus::WaitingForResource,
|
||||
)
|
||||
.await;
|
||||
let _permit = tokio::time::timeout(
|
||||
Duration::from_secs(45),
|
||||
super::h2_stream_budget::acquire(route),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| H2DownloadFailure::Connect)?
|
||||
.map_err(|_| H2DownloadFailure::Connect)?;
|
||||
let _permit = super::h2_stream_budget::acquire_with_pressure(route)
|
||||
.await
|
||||
.map_err(|_| H2DownloadFailure::Connect)?;
|
||||
super::h2_download::record_install_stage(
|
||||
request,
|
||||
crate::install::DownloadItemStatus::Downloading,
|
||||
|
||||
@@ -104,6 +104,20 @@ pub(crate) async fn acquire(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn acquire_with_pressure(
|
||||
route: &DownloadRoute,
|
||||
) -> crate::Result<H2StreamPermit> {
|
||||
crate::util::fetch::wait_for_download_resource(acquire(route))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::NetworkError(
|
||||
"HTTP/2 stream permit remained stalled for 5 minutes"
|
||||
.to_string(),
|
||||
))
|
||||
})?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Acquires a stream for the Minecraft asset batch. Assets retain a large
|
||||
/// logical worker window, but use a separate physical stream pool so they
|
||||
/// cannot consume the ordinary native content share.
|
||||
@@ -134,10 +148,25 @@ pub(crate) async fn acquire_asset(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn acquire_asset_with_pressure(
|
||||
route: &DownloadRoute,
|
||||
) -> crate::Result<H2StreamPermit> {
|
||||
crate::util::fetch::wait_for_download_resource(acquire_asset(route))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::Error::from(crate::ErrorKind::NetworkError(
|
||||
"asset HTTP/2 stream permit remained stalled for 5 minutes"
|
||||
.to_string(),
|
||||
))
|
||||
})?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::util::fetch::DownloadRouteSource;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
fn route() -> DownloadRoute {
|
||||
DownloadRoute {
|
||||
@@ -190,4 +219,40 @@ mod tests {
|
||||
MAX_H2_STREAMS_PER_AUTHORITY
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn contended_stream_wait_reports_pressure_and_resumes() {
|
||||
let route = DownloadRoute {
|
||||
url: "https://h2-pressure.example/file".to_string(),
|
||||
source: DownloadRouteSource::Official,
|
||||
is_mirror: false,
|
||||
allow_sensitive_headers: true,
|
||||
supports_range: true,
|
||||
proxy: ProxyPolicy::Direct,
|
||||
};
|
||||
let mut held = Vec::new();
|
||||
for _ in 0..MAX_H2_STREAMS_PER_AUTHORITY {
|
||||
held.push(acquire(&route).await.unwrap());
|
||||
}
|
||||
let pressure = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&pressure);
|
||||
let release = tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
drop(held);
|
||||
});
|
||||
|
||||
let permit = crate::util::fetch::wait_for_resource_with_pressure(
|
||||
acquire(&route),
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(100),
|
||||
move || {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
release.await.unwrap();
|
||||
assert!(permit.is_ok());
|
||||
assert!(pressure.load(Ordering::Relaxed) >= 1);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,19 @@ pub(crate) fn task_concurrency_limit(state: &crate::State) -> Option<usize> {
|
||||
if active_engine() == DownloadEngine::XmclCompat {
|
||||
None
|
||||
} else {
|
||||
Some(state.download_concurrency())
|
||||
Some(native_budget::task_limit(
|
||||
state.download_concurrency(),
|
||||
usize::MAX,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn single_authority_task_concurrency_limit(
|
||||
state: &crate::State,
|
||||
) -> Option<usize> {
|
||||
if active_engine() == DownloadEngine::XmclCompat {
|
||||
None
|
||||
} else {
|
||||
Some(native_budget::task_limit(state.download_concurrency(), 1))
|
||||
}
|
||||
}
|
||||
@@ -144,6 +144,15 @@ pub(crate) fn available(route: &DownloadRoute) -> usize {
|
||||
.unwrap_or(MAX_CONNECTIONS_PER_AUTHORITY)
|
||||
}
|
||||
|
||||
pub(crate) fn task_limit(
|
||||
configured_limit: usize,
|
||||
authority_count: usize,
|
||||
) -> usize {
|
||||
configured_limit.max(1).min(MAX_NATIVE_CONNECTIONS).min(
|
||||
MAX_CONNECTIONS_PER_AUTHORITY.saturating_mul(authority_count.max(1)),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -174,4 +183,11 @@ mod tests {
|
||||
drop(permits);
|
||||
assert!(try_acquire(&route).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_limit_matches_native_connection_capacity() {
|
||||
assert_eq!(task_limit(128, 1), 8);
|
||||
assert_eq!(task_limit(128, 4), 32);
|
||||
assert_eq!(task_limit(4, 1), 4);
|
||||
}
|
||||
}
|
||||
@@ -85,6 +85,8 @@ const MAX_REDIRECT_LOCATION_BYTES: usize = 8 * 1024;
|
||||
const FILE_TRANSFER_CONNECT_TIMEOUT: time::Duration =
|
||||
time::Duration::from_secs(20);
|
||||
const RESOURCE_WAIT_TIMEOUT: time::Duration = time::Duration::from_secs(45);
|
||||
const RESOURCE_PRESSURE_INTERVAL: time::Duration = time::Duration::from_secs(3);
|
||||
const RESOURCE_STALL_TIMEOUT: time::Duration = time::Duration::from_secs(300);
|
||||
#[cfg(not(test))]
|
||||
const FILE_TRANSFER_READ_TIMEOUT: time::Duration =
|
||||
time::Duration::from_secs(60);
|
||||
@@ -3894,6 +3896,73 @@ struct NativeConnectionPermit<'a> {
|
||||
_native: crate::util::download::native_budget::NativeBudgetPermit,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum ResourceWaitError {
|
||||
TimedOut,
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_resource_with_pressure<F, T, P>(
|
||||
future: F,
|
||||
pressure_interval: time::Duration,
|
||||
stall_timeout: time::Duration,
|
||||
mut on_pressure: P,
|
||||
) -> Result<T, ResourceWaitError>
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
P: FnMut(),
|
||||
{
|
||||
let pressure_interval =
|
||||
pressure_interval.max(time::Duration::from_millis(1));
|
||||
let pressure_timer = tokio::time::sleep(pressure_interval);
|
||||
let stall_timer = tokio::time::sleep(stall_timeout);
|
||||
tokio::pin!(future, pressure_timer, stall_timer);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = &mut future => return Ok(result),
|
||||
_ = &mut stall_timer => return Err(ResourceWaitError::TimedOut),
|
||||
_ = &mut pressure_timer => {
|
||||
on_pressure();
|
||||
pressure_timer.as_mut().reset(
|
||||
tokio::time::Instant::now() + pressure_interval,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn wait_for_download_resource<F, T>(
|
||||
future: F,
|
||||
) -> Result<T, ResourceWaitError>
|
||||
where
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
wait_for_resource_with_pressure(
|
||||
future,
|
||||
RESOURCE_PRESSURE_INTERVAL,
|
||||
RESOURCE_STALL_TIMEOUT,
|
||||
|| {
|
||||
if let Some(state) = crate::State::get_if_initialized() {
|
||||
state.record_download_queue_wait();
|
||||
}
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn acquire_fetch_permit_with_pressure(
|
||||
semaphore: &FetchSemaphore,
|
||||
) -> crate::Result<SemaphorePermit<'_>> {
|
||||
wait_for_download_resource(semaphore.0.acquire())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::Error::from(ErrorKind::NetworkError(
|
||||
"download permit remained stalled for 5 minutes".to_string(),
|
||||
))
|
||||
})?
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn acquire_native_connection<'a>(
|
||||
route: &DownloadRoute,
|
||||
semaphore: &'a FetchSemaphore,
|
||||
@@ -3906,6 +3975,20 @@ async fn acquire_native_connection<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
async fn acquire_native_connection_with_pressure<'a>(
|
||||
route: &DownloadRoute,
|
||||
semaphore: &'a FetchSemaphore,
|
||||
) -> crate::Result<NativeConnectionPermit<'a>> {
|
||||
wait_for_download_resource(acquire_native_connection(route, semaphore))
|
||||
.await
|
||||
.map_err(|_| {
|
||||
crate::Error::from(ErrorKind::NetworkError(
|
||||
"native download resources remained stalled for 5 minutes"
|
||||
.to_string(),
|
||||
))
|
||||
})?
|
||||
}
|
||||
|
||||
fn try_acquire_native_connection<'a>(
|
||||
route: &DownloadRoute,
|
||||
semaphore: &'a FetchSemaphore,
|
||||
@@ -5703,19 +5786,11 @@ async fn download_to_path_inner(
|
||||
None
|
||||
};
|
||||
if let Some((h2_route, h2_policy)) = h2_selection {
|
||||
let h2_permit =
|
||||
tokio::time::timeout(RESOURCE_WAIT_TIMEOUT, semaphore.0.acquire())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ErrorKind::NetworkError(
|
||||
"timed out waiting for HTTP/2 download permit"
|
||||
.to_string(),
|
||||
)
|
||||
})??;
|
||||
let h2_started = Instant::now();
|
||||
match crate::util::download::h2_download::try_download_via_h2(
|
||||
&request,
|
||||
&h2_route,
|
||||
semaphore,
|
||||
destination,
|
||||
&part_path,
|
||||
h2_policy,
|
||||
@@ -5808,7 +5883,6 @@ async fn download_to_path_inner(
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
drop(h2_permit);
|
||||
}
|
||||
|
||||
let mut official_integrity_retry = h2_failed_nonofficial.is_some();
|
||||
@@ -6147,8 +6221,6 @@ async fn download_to_path_inner(
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let mut activity = crate::State::get_if_initialized()
|
||||
.map(|state| state.begin_download_connection());
|
||||
record_install_download_started(
|
||||
&request,
|
||||
route,
|
||||
@@ -6161,25 +6233,21 @@ async fn download_to_path_inner(
|
||||
DownloadItemStatus::WaitingForResource,
|
||||
)
|
||||
.await;
|
||||
let permit_wait = tokio::time::timeout(
|
||||
RESOURCE_WAIT_TIMEOUT,
|
||||
acquire_native_connection(route, semaphore),
|
||||
);
|
||||
let permit_wait =
|
||||
acquire_native_connection_with_pressure(route, semaphore);
|
||||
let resource_wait_started = Instant::now();
|
||||
let permit = if let Some(cancellation) = request.cancellation.as_ref() {
|
||||
let permit = if let Some(cancellation) =
|
||||
request.cancellation.as_ref()
|
||||
{
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => return Err(ErrorKind::OtherError("download canceled while waiting for native resources".to_string()).into()),
|
||||
result = permit_wait => result,
|
||||
}
|
||||
} else {
|
||||
permit_wait.await
|
||||
}
|
||||
.map_err(|_| {
|
||||
ErrorKind::NetworkError(
|
||||
"timed out waiting for native download resources"
|
||||
.to_string(),
|
||||
)
|
||||
})??;
|
||||
}?;
|
||||
let mut activity = crate::State::get_if_initialized()
|
||||
.map(|state| state.begin_download_connection());
|
||||
tracing::debug!(
|
||||
route = %sanitize_url_for_log(&route.url),
|
||||
resource = "native_connection_and_fetch",
|
||||
@@ -7135,6 +7203,40 @@ mod tests {
|
||||
static H2_FALLBACK_TEST_LOCK: LazyLock<std::sync::Mutex<()>> =
|
||||
LazyLock::new(|| std::sync::Mutex::new(()));
|
||||
|
||||
#[tokio::test]
|
||||
async fn resource_wait_reports_pressure_without_losing_queue_position() {
|
||||
let pressure_events = Arc::new(AtomicUsize::new(0));
|
||||
let observed = Arc::clone(&pressure_events);
|
||||
let result = wait_for_resource_with_pressure(
|
||||
async {
|
||||
tokio::time::sleep(Duration::from_millis(35)).await;
|
||||
7
|
||||
},
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(100),
|
||||
move || {
|
||||
observed.fetch_add(1, Ordering::Relaxed);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, Ok(7));
|
||||
assert!(pressure_events.load(Ordering::Relaxed) >= 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resource_wait_keeps_a_final_stall_guard() {
|
||||
let result = wait_for_resource_with_pressure(
|
||||
std::future::pending::<()>(),
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(25),
|
||||
|| {},
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(result, Err(ResourceWaitError::TimedOut));
|
||||
}
|
||||
|
||||
async fn spawn_range_server(
|
||||
data: Arc<Vec<u8>>,
|
||||
wrong_content_range: bool,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './armor-preview-types'
|
||||
export * from './types'
|
||||
export * from './use-armor-preview'
|
||||
export * from './use-skin-preview-animation'
|
||||
export * from './use-skin-preview-controls'
|
||||
export * from './use-skin-preview-fit'
|
||||
|
||||
Reference in new issue
Block a user