Archive for the ‘.NET 4’ Category

Task.Run Etiquette Examples: Don’t Use Task.Run in the Implementation

Friday, May 8th, 2015

Az async – await dolgokkal fel lehet szabadítani pl. as ASP.NET által is használt ThreadPool szálakat, hogy míg egy hosszú ideig tartó nem CPU hanem IO intenzív folyamat fut, addig legyen szabad szál kiszolgálni a rendes, kicsi, gyors kéréseket.

De ha úgy aszinkronítunk egy blokkoló, IO intenzív kérést, hogy becsomagoljuk Task.Run-ba, akkor adtunk a sznak egy pofont, mert pont ugyanabból a ThreadPoolból vettünk el szálat, mint amit az ASP.NET is használ (feltételezve az alap TaskSchedulert használjuk). Ráadásul még context switch is lesz a szálak között, stb.

Az igazi aszinkron cuccosok (pl. .NET szerviz hívó osztályok és adatbázis kezelő osztályok) IO completion portot használnak, amivel sok blokkoló folyamatot tudnak monitorozni kevés szálon, nem minden egyes folyamathoz egy szálat használva, mint a Task.Run-os megoldás.

Bővebben a témáról itt.

Console.WriteLine blokkolás

Tuesday, November 11th, 2014

Ma megint láttam valami újat, kár, hogy bosszúság árán. Egy program logol fileba és konzolra is. VS debugger alatt futott, majd detacholtam. A detacs hatására a Console.WriteLine többet nem tért vissza, nem hibát adott, hanem blokkolta a hívást, ezzel teljen megállítva a program működését. Gondlom a VS átirányította a std outot magára, a detacs során viszont valami nem jött össze neki. Tanulság: ne tessék a debuggerel szórakozni, ha produkciós kódról is fontos futtatásról van szó.

Ha egy MVC site nem ad vissza semmit

Thursday, October 9th, 2014

De hibát se, akkor lehet egyszerűen nincs minden IIS modul feltelepítve, ami kell hozzá.

MemoryCache bug

Saturday, July 26th, 2014

A MemoryCache osztály ASP.NET hoszt alatt időnként Dispose-olja magát. Nagyon kedves. Mindezt úgy teszi, hogy nem dob semmiféle exceptiont, csak ha beleraksz valamit, nem marad benne.
4.5-ben fixálták, de 4.0-ra is van workaround vagy hotfix is. Részletek itt.

Computing a Cartesian Product with LINQ

Tuesday, April 22nd, 2014

Na, ezzel izzadtam volna, ha magamtól kell kitalálni.

Entity Framework 6 null kezelés

Tuesday, April 15th, 2014

Ha egy nullos oszlopon szűrünk, akkor előfordulhat, hogy a bemeneti paraméter is null.
where oszlop = @param

Ebben az esetben az SQL Server alapban az ansi nullságot alkalmazza, így a null != nullal, azaz nem jön vissza egy sor se. Ezért EF6-ban az SQL generátor más sqlt generál, az ilyen esetekben egy IS NULL-os ággal megoldja, hogy nullokra is menjen a keresés.
Viszont ennek drámai mellékhatásai lehetnek. Lássunk egy egyszerű példát:

using (var e = new AdventureWorks2012Entities())
{
var matthew = "Matthew";
e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Ez esik ki az EF-ből:

declare @p__linq__0 nvarchar(4000) = N'Matthew'

SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE (([Extent1].[LastName] = @p__linq__0) 
	AND ( NOT ([Extent1].[LastName] IS NULL OR @p__linq__0 IS NULL))) 
	OR (([Extent1].[LastName] IS NULL) AND (@p__linq__0 IS NULL))

Látható, hogy bool algebrával összehozták, hogy ha a paraméter null és az oszlop is null (is nullal), akkor lejönnek szépen a sorok. Viszont az ilyen query-ket utálja az sql server, nehezen tudja őket optimalizálni. option(recompile) segítene rajta, de ezt meg nem lehet kiadni EF-en keresztül.

Szerencsére vissza lehet állítani a régi kódgenerátort is:

using (var e = new AdventureWorks2012Entities())
{
    e.Configuration.UseDatabaseNullSemantics = true;
    var matthew = "Matthew";
    e.Person.FirstOrDefault(p => p.LastName == matthew);
}

Generált SQL:

	SELECT TOP (1) 
    [Extent1].[BusinessEntityID] AS [BusinessEntityID], 
    [Extent1].[PersonType] AS [PersonType], 
    [Extent1].[NameStyle] AS [NameStyle], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[MiddleName] AS [MiddleName], 
    [Extent1].[LastName] AS [LastName], 
    [Extent1].[Suffix] AS [Suffix], 
    [Extent1].[EmailPromotion] AS [EmailPromotion], 
    [Extent1].[AdditionalContactInfo] AS [AdditionalContactInfo], 
    [Extent1].[Demographics] AS [Demographics], 
    [Extent1].[rowguid] AS [rowguid], 
    [Extent1].[ModifiedDate] AS [ModifiedDate]
    FROM [Person].[Person] AS [Extent1]
    WHERE [Extent1].[LastName] = @p__linq__0

Így már egy teljesen tiszta szűrést kapunk, de ez NEM menne null bemeneti paraméterre. Ha nem is kell, hogy menjen, akkor viszont ez sokkal gyorsabb lehet, mint az első, megfelelő indexek esetén.

A két lekérdezés tervét egymás mellé rakva jól látható a különbség:
EFUseDatabaseNullSemanticsDiff

Nagyobb tábláknál a hatás sokkal radikálisabb lenne, érdemes szem előtt tartani ezt.

Rétegek közötti kommunikáció

Monday, February 10th, 2014

Tegyük fel emailt akarok küldeni egy weboldalról, amiben linkekeket kell elhelyezni, amik hivatkoznak a weboldal urljeire.
Az email generálás a business logic jellegű komponensekben van. Ezeknek nem illik kinyúlkálni HttpContextekbe meg mindenféle webes cuccokba, mert ők nem webes cuccok. De ebben az esetben mégis csak fel kéne dinamikusan deríteni, hol fut a webapp, mert urleket kell generálni.

Ha kézzel passzolom át a szükséges infót, így néz ki a hívási lánc:

ArticleController.ArticleEdit => (innentől BBL, nem web) IArticleManager.UpdateArticle => INotificationManager.ArticleModified => INotificationFormatter.SendNotification

Na, a NotificationFormatternek már tudni kell urleket gyártani, a felette futó website intrinsic property-jei alapján.
Ezt kézzel átpasszolni a teljes láncon ronda. Mi jöhet még szóba? Statikus változók, amit a webapp indulásakor pl. a global.asaxben feltöltök, és a BLL-ben kiolvasom? Nem tetszik, de egyszerű. Utálom a statikusokat, szétborítanak minden tesztet.
Létrehozni egy IWebEnvironment interfészt, amit a tesztekben fake-elek, élőben pedig szintén a web indulásakor betárazok a DI containerbe? Kicsit körmönfont, de ez egy fokkal jobban tetszik.
Ötlet?

MVC project indulás lassú

Thursday, January 30th, 2014

Normális, hogy minden egyes fordítás után fél percet kell várni, mire az mvc projekt elindul? Vagy csak nálam ilyen tetű lassú? Nem debuggerben, nem symbol issue, csak simán ránézve böngészőből. Cassinivel és iissel is. Hm?

WAS alatt hoszolt WCF szerviz

Saturday, February 2nd, 2013

Pár órám elment az életemből, mivel véletlenül abszolút elérési utat írtam be endpoint addressnek egy WAS (IIS) alatt hoszolt WCF szervizbe. Jó szokásától eltérően nem jött semmi világos hibaüzenet, mi a baja a WCF-nek.

Optional parameter zűrzavar

Monday, August 27th, 2012

Érdekes, hogy egy ártatlannak tűnő kis bővítésnek, mint a C# 4-ben bejött default paramétereknek milyen furcsaságai jönnek elő, ha beleütközik a polimorfizmusba.

Mellesleg, gondolom az mindenkinek tiszta, hogy a default paraméterek hasonló verziózási problémákat okoznak, mint az enumok: a hívó oldalba beég a konkrét érték, így a hívott library újrafordítása NINCS hatással a hívó által átadott értékre. Még nem láttam emiatt szívást, de jó észben tartani.

BCLExtensions

Monday, July 30th, 2012

Pár apró, de hasznos dolog van benne, nekem a rendezett IListben történő BinarySearch kellett belőle.

Eddig ezt csináltam:

int pos = ArrayList.Adapter(this).BinarySearch(dateTime, new Bar2DateTimeComparer());

Most már így, nem kell adaptert ráhúzni, és generikus a comparer hívás, így egy kicsit gyorsabb is:

int pos = this.BinarySearch(dateTime, new Bar2DateTimeComparer());

Test Driven Development – I like it

Wednesday, July 25th, 2012

Egyre több dolgot TDD módon írok meg. Ami szembetűnő a korábbi, először implementálok aztán ha van rá idő írok hozzá tesztet (nem fogok) módszerhez az a következő:
-Sokkal kevesebbet, nagyon sokszor egyáltalán nem debugolok. Ez eszement sok időt spóroló tevékenység, gyűlölöm, amikor 2-3 percet is nyomkodok, mire eljutok a betegnek vélt ponthoz a debuggerben. Ott aztán sokszor kilép a debugger (pont timeoutolt a web worker process), release kód miatt nem mutat változókat, továbbléptetem, aztán kezdhetem elölről (lehet mozgatni a current pointert, de ha már okoztunk mellékhatásokat hiába megyünk vissza), stb. Szeretem az advanced debuggung témát, de utálom az időt vele feleslegesen tölteni.
-A GUI-t alig indítom el, ezáltal megint nagyon sok időt takarítok meg.
-Az implementált dolgaim azonnal mennek, amikor a GUI-t is elindítom, már csak élvezni kell az eredményt.
-Hamarabb készen vagyok a feladattal. Tudom, ezt nem lehet objektíven mérni, mert nincs mihez viszonyítani. De az első két pont miatt érzésre annyi időt megtakarítunk, ami felülmúlja a tesztekre szánt időt.

Igazából a TDD nem is a tesztelésről szól. Ez komplexitás és félelem kezelés. Arról szól, hogyan implementáljuk egy bonyolult feladatot sok kicsit változaton keresztül úgy, hogy ne őrüljünk bele, ne legyen sok kudarcunk. Az már csak egy plusz, hogy sokkal jobb lesz a kódminőség.

SP vs. OR mapper

Monday, July 23rd, 2012

Az előző bejegyzésből a kommentek alapján lehet az jött le, hogy én minden dolgot OR mapperrel valósítanék meg. Csudákat, szó sincs erről.

Nézzünk egy példát. Az előző architektúra tervemben (ami prototípus majd framework is lett, nem csak terv) NHibernate és Oracle alapon készült az app. Sok más mellett tetszőleges országban használható emberneveket kellett tudni tárolni, ezért a db terv elég általános lett. Person-PersonName-PersonNamePart, ez a 3 tábla volt a fő váz. PersonNameType, PersonNamePartType, ezek enum jellegű lookup táblák.
Egy lekérdezés célja visszaadni tetszőleges név komponens alapján a beteg nevét. Azaz a PersonNamePart tábla egy oszlopában kellett keresni, ez jól indexelhető, hatékony lekérdezés. Utána jött a mókásabb rész. A PersonNamePartból vissza kellett navigálni a PersonName-re, onnan a Personre, majd a megjelenítés miatt vissza le az összes PersonName-re és onnan tovább a PersonNamePartra.
Ezt a lekérdezést először megírtam P/L SQL-ben, hogy lássam, kb. mit akarok csinálni, ez mennyire hatékony, kell-e hozzá Cluster (nem clustered, ez oracle, ez teljesen más) index az IO csökkentésére, stb.
Amikor a lekérdezéssel elégedett voltam, elkezdtem átírni az NHibernate LINQ providerére. A célom az volt, hogy a query gondolkodásmódját pontosan visszatükrözze a LINQ query, így remélhetőleg a generált sql is hasonlóan hatékony lesz. Sajnos azonban a kísérlet kudarc volt. Az NHibernate LINQ providere egyszerűen nem implementálta amit én akartam (ha jól emlékszem Take(n) után Select). Megnéztem a forrását, ott volt egy nagy büdös throw new NotImplementedException().
Mi maradt? Sp, aminek a kimeneti joinolt sorait az NHibernate alakította vissza entitásokká (ez viszont nagyon ügyes része az NHibnek.)
Azaz a lekérdezés a lehető leghatékonyabb volt, de a végén mégis a domain modellel dolgoztam, ami végül szimpatikus hibrid lett, bár jobb lett volna tisztán LINQ-ban megírni.
Mi lett volna, hogy rendesen megírták volna a LINQ providert? Megnéztem volna a generált SQL-t, és ha az megfelelően hatékony lett volna, akkor maradt volna az.
Ha nincs rá különösebb okom, én szeretem a logikákat C#-ban írni. Az objektumorientáltság miatt jól lehet szervezni az összetettebb kódokat, sokkal jobban, mint a procedurális TSQL-ben.
Lehet memóriában cache-lni dolgokat, SQL Serveren belül nem nagyon. Az appszervereket sokkal könnyebb horizontálisan skálázni mint a DB-t. Magyarul egymás mellé berakni sok azonos szervert, még ha ez marhára nem is triviális, de legalább lehetséges. Emiatt a DB-ről terhelést levenni nem kell félnetek, mindenképpen üdvös tevékenység.
Viszont, ha az adatokhoz közel kell műveleteket végezni, akkor nem fogok feleslegesen átvinni nagyszámú sort az appszerverre. Ha egy gyakran futó kritikus tranzakció hossza fontos, akkor lehet spbe rakom. Ha sok sort érint a művelet nem hozom ki őket a dbből.

Próbálok pár példát konstruálni, illusztrálandó az előbbieket.

1. Be kell olvasni 100 sort egy táblából, lefuttatni valami összetett műveletet rajta, amely kb. 1000 sornyi programkódnak felel meg, majd az eredményt visszaírni egy másik táblába, amely kb. 20 sornyi insertet jelent. OR mapper. A bonyolult logika valószínűleg erősebb érv lesz, mint az, hogy 100 sort át kell vinni a dróton, illetve, hogy 2 roundtrippel jár a művelet. Mivel a logika C#-ban lesz, könnyű lesz teszteket írni rá (a db könnyen kifake-elhető), könnyebb lesz karbantartani, ha módosítani kell.
Ha esetleg az egésznek egy tranzakcióban kell lenni, és a round tripek ideje jelentősen nyújtja a tranzakciók futási idejét, ami miatt az esetleg szigorúbb izolációs szinten futó tranzakciók elkezdenek torlódni akkor, és csakis akkor kezdenék el filózni az spn. De ezt csak méréssel lehet eldönteni, mert az sp esetén meg valószínűleg a logika lesz lassabb, így lehet többet vesztünk, mint nyerünk.

2. Be kell olvasni 5 táblából 1 sort, ezek alapján dönteni, majd visszaírni 2 táblába 1-1 sort. Az alapeset ugye az, hogy megpróbáljuk OR mapperen keresztül megoldani a dolgot, majd belátni, hogy az elég hatékony-e, ha nem akkor áttérünk sp-re. NHibernate vagy EF Extension Pack Future-ökkel az 5 select valószínűleg kiadható 1 roundtrip alatt, ha nincs közöttük függőség. Ha van, akkor lehet, hogy join-nal vagy navigácós relációkon haladva még mindig megoldható 1 roundtrip alatt. Ha nem, akkor nem. A visszaírások normális OR mapper esetén 1 roundtrip alatt végrehajthatók (batching), így 2 roundtrip alatt megvan a feladat. Valószínűleg bőven belefér, marad az OR mapper.

3. Le kell törölni sok sort, ahol a DueDate nem mai dátum. Egyértelműen sp, semmi értelme OR mapperbe behúzni a törlendő sorokat, könnyedén megfogalmazható a feltétel egy sima SQL where-ben. Hozzáteszem, NHibbel lehet delete és update műveleteket is kiadni entitás fogalmak használatával, így annál ezt se spzném, a generált sql az lenne, ami kézzel beírnék az spbe, csak direkt sqlként. Akkor meg minek sp-zzek?

4. Be kell szúrni 500 sort, nagyon gyorsan, naponta sokszor. pl. laborgépek okádják magukból az eredményeket, ezeket kell eltárolni. Ez már érdekesebb kérdés. Első körben azt mondanánk, OR mapper erre nem való. Az első gondolatom az ilyenre valamilyen bulk copy megoldás lenne, pl. SQL Server esetén SqlBulkCopy osztállyal. De pl. Oracle esetén van un. Array Binding, amivel a bulk műveletekkel közel azonos teljesítményt lehet elérni, a bulk műveletek limitációi (nem lehet trigger a táblán, stb.) nélkül. Ami meglepő, hogy a NHibernate oracle esetén kihasználja az array bindingot, és nem csak egyszerűen batcheli a műveleteket, de array bindinggel küldi be. Emiatt veszett gyorsan tud beszúrni, így NHib esetén simán OR mappert használnék erre is. Egy konkrét mérésemben 10000 sor beszúrása így nézett ki Oracle-be (fejből mondom, de a nagyságrendek jól lesznek):
1. NHibernate identity id generator (othodox SQL Serveres gondolkodásmód): 52 sec
2. ADO.NET soronkénti insert: 12 sec
3. NHibernate hilo id generátorral: 2.7 sec
4. ADO.NET Array Bindinggal: 2.2 sec
5. Direct Path Loading: 2.1 sec

Mik a tanulságok ebből a mérésből?
A. Az identity alapú ID generálás megöli az insertek teljesítményét (1. és 2. példa alapján)
B. A sok roundtrip mindenképpen káros, emiatt lett lassú 2. is, mivel itt nem volt OR mapper overhead.
C. Az NHib elképesztően gyors volt (3. példa), csak 20%-kal lassabb volt, mint az Array Binding nyersen használva (2.2/2.7).
D. Az Array Binding eszement gyors, SQL Serveren a tábla típusú paraméterekkel lehet hasonlót elérni, de ehhez nem tudok OR mapper támogatást (mivel ehhez explicit kell fogadó kódot írni, az orához NEM).
E. A bulk copy mindenhol a leggyorsabb, de az oránál ez nagyon limitált, le is van írva, hogy arra gyúrnak, hogy az array binding legyen ugyanolyan gyors, így kiválthatja azt.

Azaz, ha megfelelően optimalizált az OR mapper adatelérése, akkor még olyan esetekben is használható, ahol első körben nem jutna az eszünkbe.

5. Két tábla között kell átmozgatni sorokat, egyszerű leképezések mentén. Sp, insert-select és delete, tranzakcióban.

6. Pessimistic Offline Lock pattern implementációban volt a következő. Meg kell nézni, létezik-e lock egy erőforrásra, ez egy egy szűrt select. Ha nem, akkor beszúrni egyet, ha igen, beszúrni egy várakozó sort. Mindezt serializable izolációs szinten, mivel meg kell akadályozni fantom sorok beszúrást a select és az insert között (orának is van ilyenje, bár nem lockol mint az Sql server alapban, hanem úgy működik, mint az RCSI az SQL servernél).
Érezhetően itt észnél kell lenni, mivel ha sokszor fut le a folyamat, akkor a serializable miatt lassú lehet a dolog. Design szempontból értelemszerűen az offline lockkal védeni kívánt dolgok lekérdezése NEM a serializable tranzakcióban volt. A védendő lekérdezés előbb lefut, majd utána jön a lock fogása, és ha sikerül, megkapja a kliens az eredményt, ha nem, akkor eldobjuk a lekérdezése eredményét. Ez pazarlás, ha ütközés van, de nem számítunk sok ütközésre. Ha egy tranzakcióban lenne a két feladat torlódás lenne, ha előbb a lock, aztán a művelet, akkor meg sikertelen művelet esetén (pl. secu beint) törölni kellene a lockot, de közben már lehet más vár rá…
Ezt a feladatot OR mapperrel írtam meg. Nem tudok pontos végrehajtási időt mondani, de ha jól emlékszem kb. 20 ms volt a teljes lock vizsgálat ideje. Ha serializable szinten egy erőforráson versengenének a folyamatok, akkor másodpercenként 50-nél többet nem tudna a szerver végrehajtani. Valójában azonban szórnak a lockolandó erőforrások, így a serializable nem fog jelentős torlódást okozni (megfelelő csak indexekkel persze).
Sokan szerintem ezt kapásból SP-ben írnák meg. Mit nyernénk vele? Mit akarunk minimalizálni? Az SQL szintű tranzakció idejét. A tranzakció kb. így nézne ki:
begin tran
select …
if () insert…
else másik insert…
commit

A kérdés, ezek ideje hogy aránylik a roundtripek által megnövelt időhöz? 3 roundtrip van, 1 select, 1 valamilyen insert és egy commit. Ha a roundtrip ideje mondjuk 3ms, akkor 9 ms a roundtrip overheadje. Ha az SQL műveletek kb. 10ms-ig tartottak, akkor kétszer olyan gyors lehet az sp-s megoldás. Azaz itt elgondolkodtató a dolog, de megint, én csak konkrét mérések alapján állnék neki átrefactorolni a megoldást spre, ilyen spekulatív úton nem. Hisz ha a roundtrip valójában csak 500 usec, akkor máris 1 napig felesleges dologgal múlattuk az időt.

7. Sok id alapján kell behozni pár száz sort. Lehetsége megoldás OR-mapper future queryvel, egy batchben, de ennek hátránya, hogy pl. SQL Servernél nem lehet 1-2 ezérnél több paramétert definiálni, így a generált sql nem lesz végrehajtható. Itt spt írtunk, amely oracle collection paraméterként vette át a kulcsok listáját, belül pedig joinoltunk az alaptáblához. SQL Servernél ezt tábla típusú paraméterrel csináltam volna meg. A bemenetet elegánsan át lehetett adni, mivel az NHibben lehet saját típust definiálni, így Guid tömb típus direkben átpasszolható volt az spbe.

Más. A már említett architektúrában volt sok olyan cross-cutting concern, amit db szinten implementálni véres lett volna. Például a WCF-ből kijövő objektum gráfot módosítani kellett, mivel sor (entitás) is mező (property) szintű ACL secu miatt meg kellett metszeni a kimenete fát (gráfot, mikor-mit), property-k értékét kimaszkolni, de közben feljegyezni a maszkoltakat, szöveget nyelvi fordítását beinjektálni a gráf tetszőleges helyére, tetszőleges entitást kiegészítő adatokat belerakni a fa megfelelő részére, be kellett küldeni minden tranzakció nyitása után egy user és tran azonosítót, offline lock információt, stb. Ezek nagyon komplex feladatok voltak, amelyeket ráadásul millisecundum nagyságrendű idő alatt kellett tudni megcsinálni tízezer elem feletti gráfokra is. Ezt én dbben nem tudtam volna megcsinálni, intenzív appszerver szintű cache-eléssel, illetve kihasználva a .NET adatstruktúráit sikerrel és elegánsan (központilag implementálva, mint az AOP-ban) megoldottuk.
Ha viszont ilyen mindent keresztülvágó követelményektől hemzseg a feladat, akkor ez ember kétszer is meggondolja, hogy kilépjen az entitások, a logikai modell világából, és elkezdjen spzni, mivel az spkre ráhúzni ezeket a megoldásokat nehézkes.
Szóval az OR mapper vs. sp kérdés igen komplex, nem lehet 2 perc alatt dönteni róla, és mindkettőnek megvan a helye, feladatonként egyesével kell eldönteni, melyiket használjuk.

Miért NHibernate?

Friday, March 30th, 2012

Jeleztem korábban, hogy mióta megismertem az NHibernate-et, azóta nem nagyon foglalkozok az Entity Frameworkkel. Miért? Pár gondolatot leírok, aztán majd kifejtem őket bővebben is.

  • Sokféle kulcsgenerálást ismer. Az id generálásnak rendkívül nagy hatása van a módosítások felviteli sebességére. Eddig az assigneded és a HILO-t használtam élőben. SQL Serve Identity elfelejtve, considered harmful. Az SQL Server 2012 Sequence + HILO viszont egy jó kombináció lesz.
  • Batch-elt műveletek. Komplex objektum gráfok módosításakor ez is igen sokat jelent, mivel 1 roundtrip alatt lehet sok insert-update-delete műveletet végrehajtani. Ráadásul pl. Oracle esetén kihasználják az Array Bindingot, ami kb. olyan, mint SQL servernél a tábla típusú paraméter, így eszement gyorsan tud DML műveleteket végrehajtani.
  • Rendkívüli kibővíthetőség. Az IInterceptor interfészen keresztül mélyen bele lehet nyúlni a működésébe. Ezt pl. field szintű securityhoz használtuk ki, e nélkül nem nagyon tudtuk volna megcsinálni.
    Vagy pl. a tranzakciók megkezdése után, de még az SQL parancsok végrehajtása előtt be kellett tolni egy a kliensoldali felhasználót reprezentáló információt (audit miatt). Erre is volt kézenfekvő kibővítési pont.
    Vagy oraclenek collection típusú paramétert kellett átadni, ezt is meg tudtuk szépen oldani saját adattípus definiálásával.
  • Direkt DML műveltek. Update vagy Delete anélkül, hogy behúznánk az entitásokat memóriába. Lehet érvelni, hogy nem erre való egy O-R mapper, de ha már egyszer az ember entitásokban és nem táblákban fogalmazza meg a dolgait, egyszerűbb a DML-eknél is ezt használni. Főleg pl. egy öröklődéses esetben, amikor több táblában is kell törölni.
  • Flexibilis materializáló. Joinolt direkt SQL select kimenetét szépen bele lehet passzírozni egy objektumfába, egyszerűen.
  • Saját adattípusok használta. Az Oraclenek nincs GUID típusa, ezért RAW(16)-ként tároltuk a guidokat. Entitás oldalon viszont szeretjük a GUID-ot, ezért egy saját típussal oldottuk meg a leképezést, így mindkét oldal természetes absztrakciókkal dolgozik.
  • Enum mapping. Ha akarom intként menti el, ha akarom stringként. Hogy is van ez EF-nél?
  • Cascade műveletek gyerekekre való használata szabályozható.
  • Szabályozható, hogy csak a megváltozott property-ket updatelje, vagy mindet?
  • Van stateless sessionje (NHib Session kb. EF ObjectContext) nagytömegű adatkezeléshez
  • Tud natívan pl. tömböt adatbázisra mappelni. Azaz sorszáma van az elemeknek, ha kitörlök egy elemet, akkor a felette levő sorszámúakat automatikusan updateli.
  • Vannak current session kontérerei. Ezek pl. WCF szerviz és DI használata esetén nélkülözhetetlenek, így pl. a session élettartama a WCF OperationContexthez láncolható. Ezt annak idején letöltöttem EF-hez, elég bonyolult volt, de ez megy ott is.
  • Future Query-k: ezekkel több lekérdezést egybe lehet fűzni, hogy egy batchben hajtsa végre. EF-hez láttam ilyet a kiegészítő packben.
  • Secondary Cache. Ezt még nem használtam, de érzésre hatalmas potenciál van benne. Cache réteg, így a lekérdezések egy részét memóriából adja vissza.
  • Sokféle adatbázis támogatása.
  • Lazy properties. Betölti az entitást, de nem tölti be pl. a benne levő byte tömböt, ami egy blobra meppelődik a táblában. Csak akkor töltődik be, ha tényleg hivatkozunk a property-re. Ehhez ugyanazt a virtuális proxy megoldást használja, mint az EF.

Hirtelen ennyi jut az eszembe. Egyetlen, de ez nagy negatívum az EF-fel szemben a Linq providere. Az egyszerűen szar, félkész, még most, 2012. márciusában is. Ami kész van, az is elég ostoba, az EF-é viszont okos, tudja, mi a jó az adatbázisnak, így optimalizálja a generált SQL kódot.
Pl. az alábbi NHibernate LINQ-ból: … where !a.IsValid ezt csinálta Oracle alatt:
where not(a.IsValid).
Ez persze table scant okoz orán is. Így kellett átírni: where a.IsValid == false. Ebből lett: where a.IsValid = 0. Ez már seek. De basszus, ezt elvárnám a LINQ providertől!
Nincs a LINQ providerében outer join implementálva! Vagy amikor írtam egy allekérdezéses, Take-es majd select-es nagyobb LINQ kifejezést egyszerűen NotImplementedExceptiont kaptam, és láttam, az egyik NHib fejlesztő megindokolta, hogy ez miért jó így.
Szóval ez a része beteg, párszor LINQ helyett Criteria vagy HQL alapú lekérdezést kellett írnom, ez bosszantó, ront a jó benyomáson.

Tudom, hogy ez a bejegyzés egyesek agyát felhúzza majd. Lesz, aki utána fog nézni, hogy amiket pozitívumként írtam, valójában megvan vagy lesz EF-ben, vagy az EF Extensions packben. Lehet, nem tudok mindent én se fejből.
Nyissunk vitát, nézzük meg, melyik-miben jó, ebből mindenki tanulhat.

Gondolatok a queue alapú kliens-szerviz kommunikációhoz

Thursday, June 16th, 2011

Az előző post kommentjei alapján (amit nagyon köszönök mindenkinek) nem kaptam sok bátorítást az aszinkron, queue alapú, kérés-választ különválasztó gazdag kliens – szerviz kommunikációhoz, úgy tűnik senki nem csinált ilyet, így nem akarok úttörő lenni a témában.
Ehhez még hozzájárul, hogy hétvégén méregettem az MSMQ teljesítményét. Azért ezt, mert a WCF is erre épít, és pl. az NServiceBus is.

A tesztkód 1.5kByteos üzeneteket rak át egyik sorból a másikba. Az ötlet innen jött, csak többszálasítottam.

A tesztkód:

using System;
using System.Diagnostics;
using System.Messaging;
using System.Threading;

namespace MsmqTran
{
class Program
{
private const int NumberOfTests = 1000;
private const int MaxDop = 10;
private static readonly ManualResetEvent[] WaitForEmpty = new ManualResetEvent[MaxDop];

static void Main()
{
var q1 = new MessageQueue(@”.\private$\test_queue1″);
var q2 = new MessageQueue(@”.\private$\test_queue2″);

Console.WriteLine(“Filling source queue…”);
var b = new byte[1500];
using (var msmqTx = new MessageQueueTransaction())
{
msmqTx.Begin();
for (int i = 0; i < NumberOfTests; i++) { q1.Send(b, msmqTx); } msmqTx.Commit(); } q2.Purge(); Console.WriteLine("Starting to move data from source queue to destination queue"); var sp = Stopwatch.StartNew(); for (int i = 0; i < MaxDop; i++) { WaitForEmpty[i] = new ManualResetEvent(false); ThreadPool.QueueUserWorkItem(o => ProcessMsg(q1, q2, (ManualResetEvent)o), WaitForEmpty[i]);
}

WaitHandle.WaitAll(WaitForEmpty);

Console.WriteLine(“Duration: {0}ms, throughput: {1:F0} messages/s”, sp.ElapsedMilliseconds, 1000.0 * NumberOfTests / sp.ElapsedMilliseconds);
}

private static void ProcessMsg(MessageQueue q1, MessageQueue q2, ManualResetEvent w)
{
while (true)
{
using (var msmqTx = new MessageQueueTransaction())
{
msmqTx.Begin();

Message message;
try
{
message = q1.Receive(TimeSpan.FromMilliseconds(0), msmqTx);
}
catch (MessageQueueException e)
{
Console.WriteLine(e);
w.Set();
break;
}

q2.Send(message, msmqTx);

msmqTx.Commit();
}
}
}
}
}

A gépemen 50 tran/sec-kel megy 1 szálon, és 200 fölé nem nagyon megy. Jó, ez laptop, de relációs adatbáziskezelővel (sql server és oracle is fut a gépen) több ezer tran/seccel mennek a dolgok. Szóval ez elég gázosan lassú. Emellett csúnya leállásokról is írnak a blogokban, amikor beáll az msmq.

Marad a aszinkronított WCF egyelőre, csak a szerverről visszafelé hívásokat tervezem queue alapon megcsinálni, WCF msmq bindinggal. Így tudom értesíteni az appokat polling nélkül. Erre 3 okom van most:
1. Az offline (disconnected) pessimistic lock feloldódott, lehet szerkeszteni valamit.
2. Frissíteni kell a kliens cache-ben valamit.
3. Email jellegű üzenetküldés az appok között.

Köszönöm még egyszer az építő javaslatokat.

Érdekes .NET perf tapasztalat

Saturday, March 5th, 2011

Amikor profilerrel megnézünk egy .NET kódot sokszor megdöbbentő helyen lesz benne bottleneck.

Az alábbi kód 1% időt visz el egy nagyon processzorintenzív kódban:

if (bar.L == 0)

Ami ebben lassú, az a System.Decimal.op_Implicit(int32). A bar.L egy decimal. Érdekes, mi?
Mi a megoldás? A 0 legyen valóban decimal, de int, amit konvertálni kell:

if (bar.L == 0M)

1% kevés, de sok 1% már számít.

C# 4 Covariance

Tuesday, February 1st, 2011

.NET 4-es tanfolyamhoz írok egy prezentációt. A co és -contravarianciát próbálom érhetővé tenni. Kb. ez van a slideokon:

Variance annotations – covariance 1.

class Allat { }
class Kutya : Allat { }

interface IAllatGyar<T> where T : Allat
{
T Create();
}
class AllatGyar<T> : IAllatGyar<T> where T : Allat
{
public T Create()
{
return default(T);
}
}
AllatGyar<Kutya> kutyagyar = new AllatGyar<Kutya>();
IAllatGyar<Allat> allatGyar = kutyagyar; //cast, jogos?
Allat a = allatGyar.Create(); //mit ad vissza, típusbiztos?

Type ordering
Kutya <: Allat - A Kutya az Allat osztály leszámazottja k : Kutya - k a Kutya típus egy példánya Ha k : Kutya és Kutya <: Allat, akkor k : Allat (Liskov féle helyettesítési elv) Igaz-e, hogy ha Kutya <: Allat, akkor IAllatgyar< Kutya > <: IAllatgyar< Allat > ?
Egy Kutya példány implicit castolható Allattá. Az Allatgyar< Kutya > típusbiztosan castolható-e Allatgyar< Allat >-tá?
Ha csak kimeneti paraméterként vagy visszatérési értékként jön ki T, akkor igen, mert Kutya <: Allat, így minden kimeneti paraméternél a Allat referenciával biztonságosan elérhető egy leszármazott Kutya példány is. Ha igen, akkor T covariant, mert a típusparaméterek type orderingje érvényes a típust használó generikus típusra is, ugyanabban a sorrendben (Kutya <: Allat ==> IAllatgyar< Kutya > <: IAllatgyar< Allat >).
C# 4-ben az interfész metódus vagy delegate szignatúrában az out kulcsszó jelzi ezt:

interface IAllatgyar<out T>.

Amikor ez nem megy:

class Allat { }
class Kutya : Allat { }

interface IAllatGyar<T> where T : Allat
{
T Create();
void Valami(T t); //T befelé megy!
}

AllatGyar<Kutya> kutyagyar = new AllatGyar<Kutya>();
IAllatGyar<Allat> allatGyar = kutyagyar;

kutyagyar.Valami(new Kutya()); //Ez nyilván ok

allatGyar.Valami(new Lo()); //És ez???

Ha bemeneti paraméter is T, akkor az IAllatgyar< Allat > statikus típuson, amely valójában a IAllatgyar< Kutya > dinamikus típusra mutat átadható lenne más állat is, pl. Ló, amely szintén Allat leszármazott, de az nyilvánvaló runtime hibát okozna. Ezért nem lehet covariant T, ha az interfészen bemenetként is szerepel T.

Érhető ez így? Bármilyen javaslatot szívesen fogadok. A delegatekhez is írok persze anyagot, ez csak az eleje.

Csúnya multithreading hiba

Friday, January 21st, 2011

Double checked lockingnak indult, de bug lett belőle. Mit rontottam el?

private static volatile Dictionary> holidayDays;
private static readonly object staticLock = new object();

private Dictionary> GetHolidayDays()
{
if (holidayDays == null)
{
lock (staticLock)
{
if (holidayDays == null)
{
holidayDays = new Dictionary>();
FillTradingHours(holidayDays, “HOL”);
}
}
}
return holidayDays;
}

Dynamic sebesség újra

Monday, May 17th, 2010

Hogy ne a levegőbe beszéljek:

using System;
using System.Diagnostics;
using System.Reflection;

namespace DynamicTypeTest
{
class Program
{
static void Main(string[] args)
{
object target = “alma”;
object arg = “m”;

string a2 = (string)arg;

Stopwatch w = Stopwatch.StartNew();

const int callNumber = 1000 * 1000;
for (int i = 0; i < callNumber; i++) { Type[] argTypes = new Type[] { typeof(string) }; MethodInfo mi = target.GetType().GetMethod("Contains", argTypes); object[] oa = new object[] { a2 }; bool b = (bool)mi.Invoke(target, oa); } w.Stop(); Console.WriteLine("Reflection hívási idő: {0}", w.Elapsed); double elapsedTickForReflection = w.ElapsedTicks; w.Restart(); for (int i = 0; i < callNumber; i++) { bool b = ((dynamic)target).Contains(a2); } w.Stop(); Console.WriteLine("Dynamic hívási idő: {0}", w.Elapsed); Console.WriteLine("A dynamic {0}x gyorsabb volt.", elapsedTickForReflection / w.ElapsedTicks); } } } [/source] [source='C'] Reflection hívási idő: 00:00:02.5393161 Dynamic hívási idő: 00:00:00.2049100 A dynamic 12.3923444658829x gyorsabb volt. [/source] Ha ügyesebbek vagyunk, és kivesszük a ciklusból a reflection előkészítését: [source='C#'] using System; using System.Diagnostics; using System.Reflection; namespace DynamicTypeTest { class Program { static void Main(string[] args) { object target = "alma"; object arg = "m"; string a2 = (string)arg; Stopwatch w = Stopwatch.StartNew(); Type[] argTypes = new Type[] { typeof(string) }; MethodInfo mi = target.GetType().GetMethod("Contains", argTypes); object[] oa = new object[] { a2 }; const int callNumber = 1000 * 1000; for (int i = 0; i < callNumber; i++) { bool b = (bool)mi.Invoke(target, oa); } w.Stop(); Console.WriteLine("Reflection hívási idő: {0}", w.Elapsed); double elapsedTickForReflection = w.ElapsedTicks; w.Restart(); for (int i = 0; i < callNumber; i++) { bool b = ((dynamic)target).Contains(a2); } w.Stop(); Console.WriteLine("Dynamic hívási idő: {0}", w.Elapsed); Console.WriteLine("A dynamic {0}x gyorsabb volt.", elapsedTickForReflection / w.ElapsedTicks); } } } [/source] [source='C'] Reflection hívási idő: 00:00:01.2058747 Dynamic hívási idő: 00:00:00.2044023 A dynamic 5.89951388765798x gyorsabb volt. [/source] Jó ez, szeretjük, pedig nem is COMolunk.

Miért szeretem a dynamic típust reflection helyett?

Monday, May 17th, 2010

Ezért:

var allEntities = (IEnumerable)reposType.GetMethod(“GetAll”, new Type[] { typeof(string[]) }).Invoke(repos, new object[] { includes });

vs.

var allEntities = (IEnumerable)repos.GetAll(includes);

Emellett a dynamic vagy 10x gyorsabb, még akkor is, ha a reflectionnél cachelem a típusleírókat.