Trzecią i ostatnią część artykułu poświęcam rzadkiemu ale bardzo trudnemu w analizie problemowi, który może pojawić się w czasie adaptacji API w wykorzystaniem TaskCompletionSource.
Zacznę od takiego niewinnego kawałku kodu:
class Adapter
{
public Task<int> GetNextInt()
{
var api = new CallbacksApi();
var taskCompletionSource = new TaskCompletionSource<int>();
api.GetNextInt((result, exception) =>
{
if (exception != null)
{
taskCompletionSource.SetException(exception);
}
else
{
taskCompletionSource.SetResult(result);
}
});
return taskCompletionSource.Task;
}
}
Kod wydaje się poprawny. Co lepsze będzie działać w czasie testów dymnych, prawdopodobnie przejdzie testy funkcjonalne. Kod ten może jednak powodować pojawianie się błędów typu: „Ta operacja czasami nie kończy się przez pół godziny”, „aplikacja chodzi wolno” albo „program się zawiesza”. Co gorsza nikt nie będzie w stanie dostarczyć powtarzalnego scenariusza.
Winnym okazuje się być Garbage Collector, który może w tym przypadku błędnie stwierdzić, że nikt nie potrzebuje obiektu CallbacksApi, bo nikt nie posiada do niego referencji.
Pierwszym sposobem „obejścia” tego problemu może być „przytrzymanie” referencji do API w jakimś obiekcie, który żyje dłużej. W kodzie powyżej może być to klasa obiekt typu Adapter.
class Adapter
{
private CallbacksApi _api = new CallbacksApi();
public Task<int> GetNextInt()
{
var taskCompletionSource = new TaskCompletionSource<int>();
_api.GetNextInt((result, exception) =>
{
if (exception != null)
{
taskCompletionSource.SetException(exception);
}
else
{
taskCompletionSource.SetResult(result);
}
});
return taskCompletionSource.Task;
}
}
Sprawa się komplikuje gdy nowe CallbacksApi musi być tworzone z każdym wywołaniem GetNextInt. Brzydkim ale szybkim obejściem tego problemu jest użycie kolekcji:
class Adapter
{
private List<CallbacksApi> _apiReferences = new List<CallbacksApi>();
private object _apiReferencesLockRoot = new object();
public Task<int> GetNextInt()
{
var api = new CallbacksApi();
var taskCompletionSource = new TaskCompletionSource<int>();
lock(_apiReferencesLockRooti)
{
_apiReferences.Add(api);
}
api.GetNextInt((result, exception) =>
{
lock(_apiReferencesLockRooti)
{
_apiReferences.Add(api);
}
if (exception != null)
{
taskCompletionSource.SetException(exception);
}
else
{
taskCompletionSource.SetResult(result);
}
});
return taskCompletionSource.Task;
}
}
Ten kod działa ale wszyscy się chyba zgodzą że najładniejszy to on nie jest. W dodatku jeżeli referencja do obiektu typu Adapter nie jest nigdzie przetrzymywana przez dłuższy czas to powyższe rozwiązania i tak nie zadziałają. Na szczęście Microsoft pomyślał o tym problemie i TaskCompletionSource posiada dodatkowy parametr konstruktora „state”, którego można przekazać cokolwiek. Obiekt ten jest przypisany do właściwości AsyncState wynikowego Taska, przez co GC zostawi go w spokoju.
class Adapter
{
public Task<int> GetNextInt()
{
var api = new CallbacksApi();
var taskCompletionSource = new TaskCompletionSource<int>(api);
api.GetNextInt((result, exception) =>
{
if (exception != null)
{
taskCompletionSource.SetException(exception);
}
else
{
taskCompletionSource.SetResult(result);
}
});
return taskCompletionSource.Task;
}
}
Kod klas, które stworzyłem na potrzeby tego wpisu znajduje się w tym punkie Githuba, natomiast testy do nich tutaj. Testy zwracają oczekiwany wynik tylko po zbudowaniu projektu w trybie Release, a właściwie to nie do końca zwracają. Test dla klasy FixedAdapter przechodzi i tak ma być. Test dla klasy BrokenAdapter nie przechodzi i to także jest zgodne z moimi oczekiwaniami.
Mam natomiast problem z testem dla klasy WorkedAroundAdapter, teoretycznie linijka Assert.NotNull(adapter) powinna sprawić że test przejdzie, ten jednak przejść nie chce i nie mam pojęcia dlaczego. Postanowiłem więc postawić znaczną ilość piwa, zgrzewkę 24 Tyskiego albo jego równowartość w innym gatunku, pierwszej osobie która mi to wytłumaczy.

Bardzo pomocne dzieki 😀