SSIS Script Task中client.GetAsync未等待API响应问题排查
问题描述
在SSIS script task中使用client.GetAsync(url)发起API调用时,程序不会等待API返回响应,而是直接跳转回脚本任务入口方法public void Main()。经排查疑似异步死锁,但公开可查的常规修复方案均未生效。相同代码在网页端运行正常,可正常等待响应后执行后续逻辑。
调试观测现象
- 脚本入口处获取的payload变量状态为:
ID =5, Status = WaitingForActivation, Method = "{null}", Result = "{Not yet computed}" - 调试模式下重新执行流程时可观测到2个线程:一个为当前执行线程,另一个是首次调用生成的旧线程,旧线程中已包含首次调用期望获取的响应数据,该现象成因不明。
相关代码
Script Task 入口代码
public void Main() { // TODO: Add your code here try { PackageDetails packageInfo = new PackageDetails { PackageNumber = 1234567891, Env = "Development", UserName = "USER" }; var payload = API.ListHeadByPackAsync(packageInfo); //var test = GetResponse(); Dts.TaskResult = (int) ScriptResults.Success; } catch (Exception ex) { System.Console.Write(ex.Message); Dts.TaskResult = (int) ScriptResults.Failure; } }
API调用层代码
public static class API { public static async Task<PackageDetails> ListHeadByPackAsync(PackageDetails package) { PackageDetails packageInfo = new PackageDetails(); try { using(var client = new ApiClient(requestUrl, authToken)) { var response = await client.GetAsync(); //-> 此处不会等待响应返回 } } catch (Exception err) { switch (err.Message) { //TODO: } } return packageInfo; } }
ApiClient实现代码
public class ApiClient: IDisposable { private readonly TimeSpan _timeout; private HttpClient _httpClient; private HttpClientHandler _httpClientHandler; private readonly string _baseUrl; private readonly string _credentials; //private const string MediaTypeXml = "application/csv"; public ApiClient(string baseUrl, string authToken, TimeSpan? timeout = null) { _baseUrl = baseUrl; _credentials = Base64Encode(authToken); _timeout = timeout ?? TimeSpan.FromSeconds(90); } public async Task<string> GetAsync() { EnsureHttpClientCreated(); using(var response = await _httpClient.GetAsync(_baseUrl).ConfigureAwait(continueOnCapturedContext: false)) //-> 执行完上一行代码后直接跳回Main()方法,不等待响应 { response.EnsureSuccessStatusCode(); return await response.Content.ReadAsStringAsync(); } } public void Dispose() { _httpClientHandler?.Dispose(); _httpClient?.Dispose(); } private void CreateHttpClient() { _httpClientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip }; _httpClient = new HttpClient(_httpClientHandler, false) { Timeout = _timeout }; if (!string.IsNullOrWhiteSpace(_baseUrl)) { _httpClient.BaseAddress = new Uri(_baseUrl); } _httpClient.DefaultRequestHeaders.Add("Authorization", "Basic" + " " + _credentials); } private void EnsureHttpClientCreated() { if (_httpClient == null) { //ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; CreateHttpClient(); } } public static string Base64Encode(string token) { var tokenBytes = System.Text.Encoding.UTF8.GetBytes(token); return System.Convert.ToBase64String(tokenBytes); } }
问题根因
该问题并非异步死锁,核心原因是代码未等待异步任务执行完成。SSIS Script Task的入口Main()是同步方法,在同步方法中调用返回Task类型的异步方法时,若未对返回的Task做等待操作,代码执行到var payload = API.ListHeadByPackAsync(packageInfo);时仅会触发异步方法启动,随后立刻向下执行,直接走到设置Dts.TaskResult = Success的逻辑,整个脚本任务随即执行结束,不会等待异步HTTP请求完成。
相同代码在网页端运行正常,是因为网页端异步代码通常运行在异步上下文(如async的Page生命周期、async的Controller方法)中,会天然等待await标记的任务执行完成;但SSIS的VSTA脚本环境是类控制台的同步执行上下文,不会自动等待未被显式等待的Task。
调试时观测到的旧线程残留,是因为前一次执行脚本任务时进程直接退出,异步请求仍在后台线程运行,下次调试附加进程时,上一次启动的后台线程刚好执行完成拿到响应数据,因此能在旧线程中看到预期的返回结果。
修复方案
由于SSIS脚本任务入口不支持async Main(低版本VSTA甚至不支持C# 7.1及以上的async入口写法),兼容性最高的方案是在调用异步方法时显式同步等待任务执行完成。
将Main方法中调用API的代码修改为:
var payload = API.ListHeadByPackAsync(packageInfo).GetAwaiter().GetResult();
注意:不要使用.Wait()或直接访问.Result属性,这两种写法会将原始异常包裹在AggregateException中,不利于后续错误排查,GetAwaiter().GetResult()会直接抛出原始异常。
另外代码中两处逻辑需要同步修正:
ListHeadByPackAsync方法中拿到response后未将内容赋值给packageInfo,即使等待完成也会返回空对象,需要将响应内容反序列化后赋值给返回值。GetAsync方法中response.Content.ReadAsStringAsync()建议同样加上.ConfigureAwait(false),和前置逻辑保持一致,避免不必要的上下文捕获。
内容的提问来源于stack exchange,提问作者QuickSilver

