برنامه نویسی

الگوی حماسه آسان شد – انجمن DEV

این قسمت 2 از یک سریال است. برای مطالعه در مورد اقدامات جبرانی، اقدامات جبرانی، بخشی از یک صبحانه کامل با حماسه را بررسی کنید.

برنامه ریزی سفر با حماسه اما بدون چمدان

بنابراین آخرین باری که خانواده شما به پارک محلی رفتند، همه در مورد الگوی طراحی ساگا صحبت می‌کردند، و اکنون می‌خواهید بدانید که این الگو چیست، آیا باید آن را بخشی از طراحی سیستم توزیع‌شده خود کنید و چگونه آن را پیاده‌سازی کنید. همانطور که همه ما می دانیم، طراحی نرم افزار کاملاً شیک است1 روندها2.

قضیه حماسه ها

اگر تعجب می کنید که آیا الگوی حماسه برای سناریوی شما مناسب است، از خود بپرسید: آیا منطق شما شامل چندین مرحله است که برخی از آنها شامل ماشین‌ها، سرویس‌ها، خرده‌ها یا پایگاه‌های داده می‌شوند که اجرای جزئی برای آنها نامطلوب است.? به نظر می رسد، اینجا دقیقاً جایی است که حماسه ها مفید هستند. شاید در حال بررسی موجودی، شارژ کارت اعتباری کاربر، و سپس انجام سفارش هستید. شاید شما در حال مدیریت یک زنجیره تامین هستید. الگوی حماسه مفید است زیرا اساساً به عنوان یک ماشین حالت است که پیشرفت برنامه را ذخیره می کند، از چندین بار شارژ کارت اعتباری جلوگیری می کند، در صورت لزوم برگردانده می شود و دقیقاً می داند که چگونه در صورت قطع برق، به طور ایمن در حالت ثابت از سر گرفته شود.

یک مثال متداول مبتنی بر زندگی که برای توضیح نحوه جبران شکست ها توسط الگوی حماسه استفاده می شود، برنامه ریزی سفر است. فرض کنید برای خیس کردن باران در قلمرو Duwamish، سیاتل، خارش دارید. باید یک بلیط هواپیما بخرید، یک هتل رزرو کنید و یک بلیط برای تجربه سفر با کوله پشتی در کوه رینیر تهیه کنید. هر سه این وظایف با هم همراه هستند: اگر نمی توانید بلیط هواپیما بخرید، دلیلی برای گرفتن بقیه وجود ندارد. اگر بلیط هواپیما گرفتید اما جایی برای اقامت ندارید، می خواهید آن رزرو هواپیما را لغو کنید (یا رزرو هتل را دوباره امتحان کنید یا جای دیگری برای اقامت پیدا کنید). در نهایت، اگر نمی‌توانید آن سفر کوله‌پشتی را رزرو کنید، واقعاً دلیل دیگری برای آمدن به سیاتل وجود ندارد، بنابراین ممکن است همه چیز را لغو کنید. (شوخی!)

نمونه نمودار الگوی حماسه

در بالا: مدلی ساده برای جبران در مواجهه با شکست برنامه ریزی سفر.

در دنیای واقعی برنامه‌های نرم‌افزاری زیادی وجود دارد که «همه کار را انجام بده، یا به زحمت نیفتی»: اگر با موفقیت هزینه‌ای را از کاربر دریافت کنید، اما خدمات تکمیلی شما گزارش دهد که کالا تمام شده است، خواهید داشت. در صورت عدم بازپرداخت هزینه، کاربران را ناراحت کنید. اگر مشکل معکوس داشته باشید و به طور تصادفی اقلام را “رایگان” تحویل دهید، از تجارت خارج خواهید شد. اگر دستگاه هماهنگ‌کننده خط لوله پردازش داده‌های یادگیری ماشینی از کار بیفتد، اما ماشین‌های دنبال‌کننده به پردازش داده‌ها ادامه دهند و جایی برای گزارش داده‌هایشان وجود نداشته باشد، ممکن است صورتحساب منابع محاسباتی بسیار گرانی در دست داشته باشید.3. در تمام این موارد، داشتن نوعی «ردیابی پیشرفت» و کد جبرانی برای مقابله با این وظایف «همه چیز را انجام بده یا هیچ‌کدام از آن را انجام نده» دقیقاً همان چیزی است که الگوی حماسه ارائه می‌کند. در اصطلاح حماسی، به این نوع وظایف “همه یا هیچ” گفته می شود معاملات بلند مدت. این لزوماً به این معنی نیست که چنین اقداماتی برای مدت طولانی اجرا می شوند، فقط به مراحل بیشتری در زمان منطقی نیاز دارند.4 از چیزی که به صورت محلی در حال اجرا با یک پایگاه داده واحد است.

چگونه یک حماسه بسازید؟

یک حماسه از دو بخش تشکیل شده است:

  1. رفتار تعریف شده برای “عقب رفتن” در صورت نیاز به “لغو” چیزی (یعنی جبران خسارت)
  2. رفتاری برای تلاش برای پیشرفت رو به جلو (یعنی وضعیت پس انداز برای دانستن اینکه در مواجهه با شکست از کجا باید بهبود پیدا کرد)

خواننده مشتاق این وبلاگ به خاطر می آورد که اخیراً پستی در مورد اقدامات جبرانی نوشتم. همانطور که از بالا می بینید، غرامت تنها نیمی از الگوی طراحی حماسه است. نیمه دیگر، که در بالا به آن اشاره شد، اساساً مدیریت دولتی برای کل سیستم است. الگوی اقدامات جبرانی به شما کمک می کند تا بدانید که چگونه در صورت یک قدم فردی (یا در اصطلاح زمانی، فعالیت) شکست می خورد. اما اگر کل سیستم پایین می رود؟ پشتیبان گیری را از کجا شروع می کنید؟ از آنجایی که هر مرحله ممکن است جبرانی نداشته باشد، مجبور خواهید بود بهترین حدس خود را بر اساس جبران‌های ذخیره شده انجام دهید. الگوی حماسه مکان فعلی شما را ردیابی می کند تا بتوانید به سمت پیشرفت رو به جلو حرکت کنید.

بنابراین چگونه می توانم حماسه ها را در کد خودم پیاده سازی کنم؟

خیلی خوشحالم که پرسیدی

به جلو خم می شود

در گوش زمزمه می کند

این یک سوال کمی حقه است زیرا با اجرای کد خود با Temporal، شما بطور خودکار وضعیت خود را ذخیره کنید و در هر سطحی برای شکست تلاش کنید. این بدان معناست که الگوی حماسه با Temporal به سادگی کدگذاری جبرانی است که می خواهید در هنگام یک قدم بردارید (فعالیت) شکست می خورد. پایان.

چرا که پشت این جادو، Temporal است، با طراحی، به طور خودکار پیشرفت برنامه شما را پیگیری می کند و می تواند در مواجهه با شکست فاجعه بار از جایی که متوقف شده است، ادامه دهد. علاوه بر این، Temporal در صورت شکست، فعالیت‌ها را دوباره امتحان می‌کند، بدون اینکه نیازی به اضافه کردن کدی فراتر از تعیین خط‌مشی سعی مجدد داشته باشید، به عنوان مثال:

RetryOptions retryoptions = RetryOptions.newBuilder()
       .setInitialInterval(Duration.ofSeconds(1))
       .setMaximumInterval(Duration.ofSeconds(100))
       .setBackoffCoefficient(2)
       .setMaximumAttempts(500).build();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای کسب اطلاعات بیشتر در مورد نحوه عملکرد این خودکار، منتظر پست آینده من در مورد رقص و ارکستراسیون، دو روش رایج اجرای حماسه باشید.

بنابراین برای بیان منطق سطح بالای برنامه من با هر دو مرحله رزرو تعطیلات به علاوه غرامت هایی که می خواهم در صورت شکست دریافت کنم، در شبه کد به شکل زیر است:

try:
   registerCompensationInCaseOfFailure(cancelHotel)
   bookHotel
   registerCompensationInCaseOfFailure(cancelFlight)
   bookFlight
   registerCompensationInCaseOfFailure(cancelExcursion)
   bookExcursion
catch:
   run all compensation activities
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در جاوا، Saga کلاس پاداش ها را برای شما پیگیری می کند:

@Override
public void bookVacation(BookingInfo info) {
   Saga saga = new Saga(new Saga.Options.Builder().build());
   try {
       saga.addCompensation(activities::cancelHotel, info.getClientId());
       activities.bookHotel(info);

       saga.addCompensation(activities::cancelFlight, info.getClientId());
       activities.bookFlight(info);

       saga.addCompensation(activities::cancelExcursion, 
                            info.getClientId());
       activities.bookExcursion(info);
   } catch (TemporalFailure e) {
       saga.compensate();
       throw e;
   }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در SDK های زبان دیگر می توانید به راحتی آن را بنویسید addCompensation و compensate خود عمل می کند. این یک نسخه در Go است:

func (s *Compensations) AddCompensation(activity any, parameters ...any) {
    s.compensations = append(s.compensations, activity)
    s.arguments = append(s.arguments, parameters)
}

func (s Compensations) Compensate(ctx workflow.Context, inParallel bool) {
    if !inParallel {
        // Compensate in Last-In-First-Out order, to undo in the reverse order that activies were applied.
        for i := len(s.compensations) - 1; i >= 0; i-- {
            errCompensation := workflow.ExecuteActivity(ctx, s.compensations[i], s.arguments[i]...).Get(ctx, nil)
            if errCompensation != nil {
                workflow.GetLogger(ctx).Error("Executing compensation failed", "Error", errCompensation)
            }
        }
    } else {
        selector := workflow.NewSelector(ctx)
        for i := 0; i < len(s.compensations); i++ {
            execution := workflow.ExecuteActivity(ctx, s.compensations[i], s.arguments[i]...)
            selector.AddFuture(execution, func(f workflow.Future) {
                if errCompensation := f.Get(ctx, nil); errCompensation != nil {
                    workflow.GetLogger(ctx).Error("Executing compensation failed", "Error", errCompensation)
                }
            })
        }
        for range s.compensations {
            selector.Select(ctx)
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کد Go سطح بالا از مراحل و جبران ها بسیار شبیه به نسخه جاوا خواهد بود:

func TripPlanningWorkflow(ctx workflow.Context, info BookingInfo) (err error) {
   options := workflow.ActivityOptions{
       StartToCloseTimeout: time.Second * 5,
       RetryPolicy:         &temporal.RetryPolicy{MaximumAttempts: 2},
   }

   ctx = workflow.WithActivityOptions(ctx, options)

   var compensations Compensations

   defer func() {
       if err != nil {
           // activity failed, and workflow context is canceled
           disconnectedCtx, _ := workflow.NewDisconnectedContext(ctx)
           compensations.Compensate(disconnectedCtx, true)
       }
   }()

   compensations.AddCompensation(CancelHotel)
   err = workflow.ExecuteActivity(ctx, BookHotel, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   compensations.AddCompensation(CancelFlight)
   err = workflow.ExecuteActivity(ctx, BookFlight, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   compensations.AddCompensation(CancelExcursion)
   err = workflow.ExecuteActivity(ctx, BookExcursion, info).Get(ctx, nil)
   if err != nil {
       return err
   }

   return err
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این دنباله کدهای سطح بالا در بالا Temporal نامیده می شود جریان کار. وهمانطور که قبلاً ذکر شد، با اجرای با Temporal، لازم نیست نگران اجرای هیچ یک از حسابداری برای ردیابی پیشرفت خود از طریق منبع یابی رویداد یا اضافه کردن منطق امتحان مجدد و راه اندازی مجدد باشیم، زیرا همه این موارد رایگان است. بنابراین هنگام نوشتن کدی که با Temporal اجرا می شود، فقط باید نگران نوشتن جبران باشید و بقیه به صورت رایگان ارائه می شود.

ناتوانی

خوب، خوب، یک چیز دوم برای “نگرانی” وجود دارد. همانطور که ممکن است به خاطر داشته باشید، حماسه ها از دو بخش تشکیل شده اند، بخش اول آن جبران هایی است که قبلا کدگذاری کردیم. بخش دوم، “تلاش برای پیشرفت رو به جلو” شامل تلاش مجدد بالقوه یک فعالیت در مواجهه با شکست است. بیایید یکی از آن مراحل را بررسی کنیم، درست است؟ Temporal تمام کارهای سنگین تلاش مجدد و پیگیری پیشرفت کلی شما را انجام می دهد، اما از آنجایی که کد را می توان دوباره امتحان کرد، شما، برنامه نویس، باید مطمئن شوید که هر فعالیت زمانی انجام می شود. ناتوان. این به معنای نتیجه مشاهده شده از bookFlight خواه یک بار خوانده شود یا چند بار یکسان است. برای اینکه این کمی دقیق تر شود، تابعی که مقداری فیلد را تعیین می کند foo=3 ناتوان است زیرا پس از آن foo مهم نیست چند بار با آن تماس بگیرید 3 خواهد بود. کارکرد foo += 3 ناتوان نیست زیرا ارزش foo به تعداد دفعاتی که تابع شما فراخوانی می شود بستگی دارد. گاهی اوقات می‌تواند ظریف‌تر به نظر برسد: اگر پایگاه داده‌ای دارید که امکان تکرار رکوردها را فراهم می‌کند، تابعی که فراخوانی می‌کند. INSERT INTO foo (bar) VALUES (3) با خیال راحت به تعداد دفعاتی که شما آن را فراخوانی می کنید رکوردهای زیادی در جدول شما ایجاد می کند و بنابراین بی قدرت نیست. اجرای ساده توابعی که ایمیل می فرستند یا پول انتقال می دهند نیز به طور پیش فرض بی قدرت نیستند.

اگر در حال حاضر به آرامی عقب نشینی می کنید زیرا برنامه دنیای واقعی شما کارهای بسیار پیچیده تری نسبت به تنظیم شده انجام می دهد foo=3، شجاع باش. راه حلی وجود دارد. می توانید از یک شناسه متمایز به نام an استفاده کنید کلید ناتوانی، یا گاهی اوقات به نام a referenceId یا چیزی مشابه برای شناسایی منحصر به فرد یک تراکنش خاص و اطمینان از اینکه تراکنش رزرو هتل به طور موثر یک بار انجام می شود. روشی که ممکن است این کلید عدم توانایی بر اساس نیازهای برنامه شما تعریف شود. در برنامه برنامه ریزی سفر، clientId، یک میدان در BookingInfo برای شناسایی منحصر به فرد تراکنش ها استفاده می شود.

type BookingInfo struct {
   Name     string
   ClientId string
   Address  string
   CcInfo   CreditCardInfo
   Start    date.Date
   End      date.Date
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

شما نیز احتمالاً آن را دیده اید clientId برای ثبت جبران در کد گردش کار جاوا بالا استفاده می شود:

saga.addCompensation(activities::cancelHotel, info.getClientId());
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با این حال، با استفاده از clientId زیرا کلید ما یک فرد خاص را از رزرو همزمان بیش از یک تعطیلات محدود می کند. این احتمالاً همان چیزی است که ما می خواهیم. با این حال، برخی از برنامه های کاربردی تجاری ممکن است تصمیم بگیرند که یک کلید عدم توانایی را با ترکیب کردن آنها ایجاد کنند clientId و workflowId اجازه دادن به بیش از یک تعطیلات در یک بار رزرو شده برای هر مشتری. اگر می‌خواهید یک کلید بی‌توانی واقعاً منحصربه‌فرد داشته باشید، می‌توانید یک UUID را به گردش کار ارسال کنید. انتخاب بر اساس نیازهای برنامه شما به عهده شماست.

بسیاری از API های شخص ثالث که پول را مدیریت می کنند، کلیدهای عدم توانایی را برای همین منظور می پذیرند. اگر می‌خواهید خودتان چنین چیزی را پیاده‌سازی کنید، از نوشتن‌های اتمی برای نگه‌داشتن یک رکورد از کلیدهای idempotency که تاکنون دیده‌اید استفاده کنید و اگر کلید idempotency آن در مجموعه «از قبل دیده‌شده» است، عملیاتی را انجام ندهید.

مزایا در مقابل پیچیدگی

الگوی حماسه به کد شما پیچیدگی می بخشد، بنابراین مهم است که آن را در کد خود پیاده نکنید فقط زیرا شما میکروسرویس دارید. با این حال، اگر نیاز به انجام کاری (مانند رزرو یک سفر با بلیط هواپیما و هتل) دارید که شامل خدمات متعدد و اجرای جزئی است، در واقع موفقیت آمیز نیست، یک حماسه دوست شما خواهد بود. علاوه بر این، اگر حماسه خود را در حال در حال شدن یافتید به ویژه ممکن است زمان آن رسیده باشد که در نحوه تقسیم بندی میکروسرویس های خود تجدید نظر کنید و آستین های اولیه را برای Refactor بالا بزنید. به طور کلی، Temporal پیاده‌سازی الگوی حماسه در کد شما را نسبتاً بی‌اهمیت می‌کند، زیرا فقط باید جبران‌های مورد نیاز برای هر مرحله را بنویسید. منتظر پست بعدی من باشید، جایی که من به حماسه ها و سناریوهای اشتراک می پردازم، جایی که Temporal به ویژه در کاهش پیچیدگی هنگام کار با حماسه می درخشد.

مخزن کاملی که از کد ذکر شده در این مقاله استفاده می کند را می توانید در GitHub بیابید:

اگر می‌خواهید سایر آموزش‌های حماسه با استفاده از Temporal را ببینید، لطفاً منابع زیر را بررسی کنید:

علاوه بر این، یکی از همکاران من، دومینیک تورنو، مقدمه ای از حماسه در یوتیوب ارائه کرد.

در دوره ها، آموزش ها، اسناد و ویدیوهای ما درباره Temporal بیشتر بیاموزید.

یادداشت


  1. بدیهی است که سیستم خود را فقط به این دلیل که داغ جدید است دوباره طراحی نکنید. مگر اینکه یک فریمورک جدید جاوا اسکریپت باشد. سپس npm install آن بسته جدید با عجله کافی. 😜 ↩

  2. نگران نباشید، حماسه ها یک روند نیستند. آنها از دهه 80 در پایگاه داده ها وجود داشته اند. با دانستن اینکه پروژه شما دارای ظرافت کلاسیک در طراحی است، می توانید راحت باشید. ↩

  3. نه اینکه نویسنده مطلقاً هیچ تجربه ای با این سناریو داشته باشد. سرفه در قیمت یک ماشین نو 😬 ↩

  4. زمان منطقی مفهومی در محاسبات توزیع‌شده برای توصیف زمان‌بندی رویدادهایی است که روی ماشین‌های مختلف در محاسبات توزیع‌شده اتفاق می‌افتند، زیرا ممکن است ماشین‌ها یک ساعت جهانی همزمان فیزیکی نداشته باشند. زمان‌بندی منطقی صرفاً ترتیب علّی رویدادهایی است که در این ماشین‌ها رخ داده‌اند. در مورد تراکنش‌های بلندمدت، اساساً به داشتن «مراحل» زیادی که در ماشین‌های مختلف انجام می‌شود خلاصه می‌شود. ↩

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا