תפיסת שגיאות בזמן אתחול הLogger

Log-ים הם ללא ספק חלק חשוב ועיקרי בניהול ומעקב אחרי הריצה של הקוד שאנחנו כותבים. בלי הlog-ים אנחנו מרגישים הרבה פעמים, בצדק, חסרי אונים. זאת גם הסיבה שלבחור נכון את המיקום והתוכן שלהם דורש מחשבה ותכנון.
לאחרונה יצא לי להיתקל בService שנכשל בשלב לא צפוי – בשלב האתחול של הLogger. לא תמיד אנחנו משקיעים מחשבה על כישלון שיכול להיווצר בשלב הזה. אני אדגים פה איך בעיה כזו יכולה לקרות, וכמובן פתרונות אפשריים. המטרה הראשית של הפוסט היא תכלס להעלות את המודעות לזה, אז גם אם קראתם עד כאן כנראה שאני את שלי עשיתי 🙂

נתחיל בדוגמה לקוד בעייתי שכזה:

using System;
using System.IO;
using Serilog;
using Serilog.Core;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace example
{
    class Program
    {
        public static void Main()
        {
            string loggerConfiguration = File.ReadAllText(@"C:\my-project\config.json");
            AppConfiguration config = JsonSerializer.Deserialize<AppConfiguration>(loggerConfiguration);
            Logger logger = GetLogger(config);
            logger.Information("Hello World");
        }

        private static Logger GetLogger(AppConfiguration config)
        {
            return new LoggerConfiguration()
                                .WriteTo.Console()
                                .WriteTo.File(config.LogFilePath)
                                .CreateLogger();
        }
    }

    internal class AppConfiguration
    {
        public string LogFilePath { get; set; }
    }
}

אז השגיאה המתבקשת תתקבל אם למשל אין קובץ בשם config.json. הכל טוב ויפה אם אני מריץ את הקוד מהconsole למשל – אני אקבל את השגיאה והיא די ברורה:

$ dotnet run
Unhandled exception. System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\my-project\config.json'.
   at System.IO.FileStream.ValidateFileHandle(SafeFileHandle fileHandle)
   at System.IO.FileStream.CreateFileOpenHandle(FileMode mode, FileShare share, FileOptions options)
   at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.StreamReader..ctor(String path, Encoding encoding, Boolean detectEncodingFromByteOrderMarks)
   at System.IO.File.InternalReadAllText(String path, Encoding encoding)
   at System.IO.File.ReadAllText(String path)
   at example.Program.Main() in C:\...\Program.cs:line 14

אבל מה אם זה היה service? אם אני למשל מוסיף את אותו exe עם אותו קוד מלמעלה בתור Service בWindows ומנסה להדליק אותו – אני חוטף:

דוגמה לשגיאה שנזרקת כשהservice לא מצליח לעלות

עכשיו, אני יכול תכלס להיכנס לEvent Viewer, ולראות שם שורה כזאת:

דוגמה לשורה בEventLog

ואם אני אפתח את הפרטים שלה, אני אמצא את את אותה השגיאה שקיבלתי למעלה פחות או יותר.
יש עם זה לפחות שתי בעיות שאני יכול לחשוב עליהן כרגע:

  1. קצת קשה ולא נוח למצוא את השגיאה, הייתי רוצה לראות למשל את השם של האפליקציה שלי במקום “.NET Runtime”.
  2. לפעמים יש שגיאה שבברירת מחדל נראות הרבה פחות טוב (למשל בעיות עם טעינת dll-ים ודברים כיפיים בסגנון). הייתי רוצה שתהיה לי יותר שליטה על מה מודפס ואיך.

כדי לטפל בבעיות למעלה יש כמה דברים שאפשר לעשות. דבר אחד למשל הוא לתפוס את השגיאה ולכתוב אותה לקובץ. זה גם קצת בעייתי, כי אם למשל יש לי בעיה במערכת הקבצים שבגללה אני לא מצליח ליצור את הlogger – מי אמר שאצליח לכתוב למערכת הקבצים?

הפיתרון שאני אוהב וחושב שהוא מתאים הוא לכתוב event דומה לevent שגם ככה נזרק, אבל לכתוב אותו בצורה הנוחה לי. דוגמה לקוד שעושה את זה:

using System;
using System.IO;
using Serilog;
using Serilog.Core;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace example
{
    class Program
    {
        public static void Main()
        {
            Logger logger = null;
            try
            {
                logger = GetLogger();
            }
            catch (Exception e)
            {
                EventLog.WriteEntry("My-Application", $"Got an error while initializing logger: {e}", EventLogEntryType.Error);
                throw e;
            }
            
            logger.Information("Hello World");
        }

        private static Logger GetLogger()
        {
            string loggerConfiguration = File.ReadAllText(@"C:\my-project\logging-config.json");
            LoggingConfiguration config = JsonSerializer.Deserialize<LoggingConfiguration>(loggerConfiguration);
            return new LoggerConfiguration()
                                .WriteTo.Console()
                                .WriteTo.File(config.LogFilePath)
                                .CreateLogger();
        }
    }

    internal class LoggingConfiguration
    {
        public string LogFilePath { get; set; }
    }
}

הקוד עובד ואני מקבל תוצאה יפה –

דוגמה לשורה בEventLog שמופיעה עם שם האפליקציה שנתתי

בתוך הevent מופיעה ההודעה שזרקתי.

כמה הערות על הקוד החדש:

  1. הכנסתי הפעם את כל הלוגיקה של טעינת הקונפיגורציה של הלוגים, קריאת הקובץ וכו’ לפנוקציה אחת – הכוונה היא להכניס את כל הנקודות בהן אני יכול להכשל למקום מרוכז.
  2. אחרי הכתיבה לEventLog אני זורק את השגיאה – אני רוצה להקריס את התוכנית.
  3. כדי לכתוב לEventLog, לפחות בdotnet core, יש צורך להתקין Nuget שזמין בקישור הזה.
  4. התוכנית צריכה לרוץ כAdministrator כדי שאפשר יהיה לכתוב לEventLog.

בהצלחה 🙂

Tuple-ים בC#

Tuple הוא פיצ’ר שימושי של C#, שאני רואה לא מנוצל בצורה מספיק טובה. בפוסט הזה אני אנסה לכתוב קצת על מה זה Tuple, ובעיקר אנסה להדגים דרך דוגמאות קוד איך שימוש בTuple-ים יכול להפוך את הקוד שלנו לפשוט יותר.

Tuple הוא בעצם מבנה נתונים שיכול להכיל קבוצה של ערכים מסוגים שונים. כך למשל אנחנו יכולים להגדיר Tuple שמכיל שם (string) וגיל (int):

var person = ("Avi", 30);

אגב, ניתן גם לתת שמות לשדות – ככה שבמקום קוד בסגנון הזה:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person (string name, int age)
    {
        Name = name;
        Age = age;
    }
}

class Program
{
    public static void Main()
    {
        var person = new Person("Avi", 30);
        Console.WriteLine($"{person.Name} is {person.Age} years old");
    }
}

אפשר לכתוב קוד קצר בהרבה (ובעצם לוותר על הגדרת המחלקה)

class Program
{
    public static void Main()
    {
        var person = (Name: "Avi", Age: 30);
        Console.WriteLine($"{person.Name} is {person.Age} years old");
    }
}

שימוש נוסף לTuple הוא החזרת כמה ערכים מפונקציה. דוגמה קצרה לפונקציה שמקבלת שם מלא ומחזירה שם פרטי ושם משפחה:

class Program
    {
        static void Main(string[] args)
        {
            var names = GetNames("Avi Cohen");
            Console.WriteLine($"First name: {names.FirstName}. Last name: {names.LastName}");
        }

        static (string FirstName, string LastName) GetNames(string fullName)
        {
            var names = fullName.Split(" ");
            return (names[0], names[1]);
        }
    }

בדוגמה הזו אפשר לראות שהשתמשתי בפיצ’ר של השמות לשדות. באותה מידה יכולתי לכתוב גם:

class Program
    {
        static void Main(string[] args)
        {
            var (firstName, lastName) = GetNames("Avi Cohen");
            Console.WriteLine($"First name: {firstName}. Last name: {lastName}");
        }

        static (string, string) GetNames(string fullName)
        {
            var names = fullName.Split(" ");
            return (names[0], names[1]);
        }
    }

איזה סינטקס עדיף? לשיפוטכם. 🙂

Concurrency לעומת Parallelism

בעולם התכנות מקובלים שני מושגים שנועדו לתאר פעולה מקבילית, בדרך כלל בהקשר של פיתוח multi threaded. שני המושגים הם Concurrency וParallelism. בעברית אנחנו מתרגמים את שתי המילים האלה לאותה מילה – מַקבִּילוּת – אבל למעשה מדובר בשני מושגים שונים. בפוסט זה אנסה להסביר את ההבדלים ביניהם.

Parallelism

אני אתחיל דווקא בהסבר המושג השני – Parallelism – כי לדעתי הוא יותר אינטואיטיבי. Parallelism זה בעצם מה שאתם (או לפחות אני) מדמיינים כשאנחנו חושבים על מקבול תהליכים. זה אומר שאם יש לי חתיכת לוגיקה שאני רוצה להריץ, אני מחלק אותה ליחידות עיבוד בלתי תלויות, ומריץ כל אחת מהן על מעבד אחר. בצורה הזו, בהינתן מחשב עם למשל 8 מעבדים, אני יכול להריץ 8 יחידות עיבוד באותו הזמן ממש. כל אחת מהן רצה באותה שניה בthread משלה על מעבד משלה. שימו לב שכדי שזה יקרה באמת וכדי שזה יהיה יעיל אני חייב שהתהליכים האלה יהיו בלתי תלויים.

Concurrency

בעיני המושג השני – Concurrency – הוא הפחות אינטואיטבי מהשניים, אז אם ההסבר לא מספיק ברור חכו לדוגמה שתבוא אחר כך ולדעתי תבהיר את הדברים.
המשמעות של Concurrency היא הרצה של שני תהליכים (או יותר) באותו זמן. ההבדל הוא שבמשמעות הזו אני יכול להריץ שני תהליכים “באותו זמן” גם אם יש לי רק מעבד אחד.
כמו שאתם וודאי יודעים – בלתי אפשרי להריץ פיזית יותר מפעולה אחת בו זמנית על מעבד בודד. אבל בהגדרה הזו אני מסתכל על הפעולות בצורה רחבה יותר, והמשמעות הכללית יותר הזו היא שאני מריץ שני תהליכים שחיים בו זמנית במחשב, גם אם הקיום המקבילי שלהם כולל context switching ודברים כאלה.

דוגמה (או יותר נכון משל)

בואו ניקח בתור דוגמה בחור נחמד, נקרא לו יוסי לשם הדוגמה. יוסי שלנו אחלה בחור, ובשל אילוצים הוא צריך להגיע לדואר כדי לקבל חבילה. לרוע מזלו של יוסי סניף הדואר שלו, איך נגיד.. לא פסגת היעילות, ולכן יש זמן המתנה לא קצר.
ליוסי יש מטלה שהוא צריך להגיש בעוד כמה שעות, וזה די מתנגש עם הלהמתין שלוש שעות לקבלת החבילה. עומדות בפניו שתי אפשרויות:

אפשרות ראשונה – יוסי ממקבל בצורה של
Concurrency

באפשרות הזו יוסי, שהוא בן אדם אחד, מנסה לבצע את שתי הפעולות במקביל. הוא לוקח את המחשב הנייד שלו איתו אל סניף הדואר, ויושב לכתוב את המטלה. בכל פעם שהתור מתקדם הוא מפסיק לכמה שניות את כתיבת המטלה, מסתכל על הפתק ובודק מתי יגיע תורו. בסוף השלוש שעות יוסי חוזר הביתה עם חבילה ביד ומטלה גמורה.

אפשרות שניה – יוסי ממקבל בצורה של Paeallelism

באפשרות הזו יוסי מחליט להשקיע קצת כסף, והוא מייפה את כוחו של אדם אחר ללכת עבורו לדואר. יוסי נשאר בבית ומסיים את המטלה. בסוף השלוש שעות יוסי עם מטלה גמורה, ועם חבילה שהגיעה אליו מהדואר.

כמו שאתם יכולים להבין, במשל כל אדם מייצג מעבד. באפשרות הראשונה אנחנו רואים שאפשר לגרום לשתי פעולות לקרות “בו זמנית”, אף על פי שבכל נקודה ספציפית של זמן יוסי מבצע פעולה אחת בלבד. כשמגיע תורו הוא מפסיק לכתוב את המטלה, וכשהוא כותב את המטלה הוא לא באמת קשוב למצב התור. בדוגמה השניה לעומת זאת יש לנו שני אנשים שמשקיעים את מלוא מרצם, כל אחד למשימה שלו.

אז מה עדיף? Concurrency? Parallelism?

כרגיל, התשובה היא תלוי.

דוגמה מהעולם האמיתי לפעולות שניתן לבצע בConcurrency הן בדרך כלל פעולות של קריאה\כתיבה למשאב. (מה שנקרא פעולות IO). בזמן שהמעבד מחכה שהדיסק או הרשת יגיבו לבקשה שלו, אין סיבה שהוא לא יפנה את הדרך ויתן לתהליכים אחרים לעבד מידע. בעידן שלנו המעבדים מאוד מאוד מהירים (ובדרך כלל יש כמה מהם), ולכן “צוואר הבקבוק” ברוב המערכות המודרניות אלה באמת הקריאות האלה. אם יודעים לגרום להם לרוץ בצורה מקבילית יותר, אפשר בדרך כלל לשפר את זמני הריצה של המערכת בצורה לא רעה בכלל.

הנושא הזה תופס תאוצה בשנים האחרונות. השפות הראשונות שהציגו יכולות להתמודד עם מצב שבו התהליך חסום על ידי בקשות קריאה \ כתיבה – להלן IO Blocking – היו JavaScript ועולם ה.NET עם async await. פייתון הצטרפה לחבורה החל מגרסה מסוימת, ושפות כמו go תומכות בזה בצורה מובנית ויעילה מאוד.