ASP.NET MVC #15
خطای data binding
همچنین زمانیکه یک صفحه خطا در VS.NET ظاهر میشود یک گزینه details دارد که میشود جزئیات را بهتر مشاهده کرد.
خطا و راهنمایی
- برای استفاده از مثال سیلورلایت پیوست شده نیاز به Silverlight toolkit دارید یا اینکه پروژه آنرا غیرفعال کنید.
خطا و راهنمایی
ارجاعی را به اسمبلی «System.Windows.Forms.DataVisualization.dll» که در پوشه bin در ریشه اصلی سورسها هست اضافه کنید. این مورد در مثالهای برنامه نیاز است.
فایل xml ایی که از قلم افتاده است پیوست شد. بعدا به سورسها اضافه خواهم کرد:
AppManifest.xml
پ.ن.
آخرین سورس را دریافت کنید. این تغییرات به آن نیز اعمال شد.
در فضایی که همواره هیچ تضمینی وجود ندارد که درخواست ارسال شدهی به یک API، همواره مسیر خود را همانطور که انتظار میرود طی کرده و پاسخ مورد نظر را در اختیار ما قرار میدهد، بیشک تلاش مجدد برای پردازش درخواست مورد نظر، به دلیل خطاهای گذرا، یکی از راهکارهای مورد استفاده خواهد بود. تصور کنید قصد طراحی یک مجموعه API عمومی را دارید، بهنحوی که مصرف کنندگان بدون نگرانی از ایجاد خرابی یا تغییرات ناخواسته، امکان تلاش مجدد در سناریوهای مختلف مشکل در ارتباط با سرور را داشته باشند. حتما توجه کنید که برخی از متدهای HTTP مانند GET، به اصطلاح Idempotent هستند و در طراحی آنها همواره باید این موضوع مدنظر قرار بگیرد و خروجی مشابهی برای درخواستهای تکراری همانند، مهیا کنید.
در تصویر بالا، حالتی که درخواست، توسط کلاینت ارسال شده و در آن لحظه ارتباط قطع شدهاست یا با یک خطای گذرا در سرور مواجه شدهاست و همچنین سناریویی که درخواست توسط سرور دریافت و پردازش شدهاست ولی کلاینت پاسخی را دریافت نکردهاست، قابل مشاهدهاست.
نکته: Idempotence یکی از ویژگی های پایهای عملیاتی در ریاضیات و علوم کامپیوتر است و فارغ از اینکه چندین بار اجرا شوند، نتیجه یکسانی را برای آرگومانهای همسان، خروجی خواهند داد. این خصوصیت در کانتکستهای مختلفی از جمله سیستمهای پایگاه داده و وب سرویسها قابل توجه میباشد.
Idempotent and Safe HTTP Methods
طبق HTTP RFC، متدهایی که پاسخ یکسانی را برای درخواستهای همسان مهیا میکنند، به اصطلاح Idempotent هستند. همچنین متدهایی که باعث نشوند تغییری در وضعیت سیستم در سمت سرور ایجاد شود، به اصطلاح Safe در نظر گرفته خواهند شد. برای هر دو خصوصیت عنوان شده، سناریوهای استثناء و قابل بحثی وجود دارند؛ بهعنوان مثال در مورد خصوصیت Safe بودن، درخواست GET ای را تصور کنید که یکسری لاگ آماری هم ثبت میکند یا عملیات بازنشانی کش را نیز انجام میدهد که در خیلی از موارد به عنوان یک قابلیت شناسایی خواهد شد. در این سناریوها و طبق RFC، باتوجه به اینکه هدف مصرف کننده، ایجاد Side-effect نبودهاست، هیچ مسئولیتی در قبال این تغییرات نخواهد داشت. لیست زیر شامل متدهای مختلف HTTP به همراه دو خصوصیت ذکر شده می باشد:
HTTP Method | Safe | Idempotent |
GET | Yes | Yes |
HEAD | Yes | Yes |
OPTIONS | Yes | Yes |
TRACE | Yes | Yes |
PUT | No | Yes |
DELETE | No | Yes |
POST | No | No |
PATCH | No | No |
Request Identifier as a Solution
راهکاری که عموما مورد استفاده قرار میگیرد، استفاده از یک شناسهی یکتا برای درخواست ارسالی و ارسال آن به سرور از طریق هدر HTTP می باشد. تصویر زیر از کتاب API Design Patterns، روش استفاده و مراحل جلوگیری از پردازش درخواست تکراری با شناسهای همسان را نشان میدهد:
در اینجا ابتدا مصرف کننده درخواستی با شناسه «۱» را برای پردازش به سرور ارسال میکند. سپس سرور که لیستی از شناسههای پردازش شدهی قبلی را نگهداری کردهاست، تشخیص میدهد که این درخواست قبلا دریافت شدهاست یا خیر. پس از آن، عملیات درخواستی انجام شده و شناسهی درخواست، به همراه پاسخ ارسالی به کلاینت، در فضایی ذخیره سازی میشود. در ادامه اگر همان درخواست مجددا به سمت سرور ارسال شود، بدون پردازش مجدد، پاسخ پردازش شدهی قبلی، به کلاینت تحویل داده می شود.
Implementation in .NET
ممکن است پیادهسازیهای مختلفی را از این الگوی طراحی در اینترنت مشاهده کنید که به پیاده سازی یک Middleware بسنده کردهاند و صرفا بررسی این مورد که درخواست جاری قبلا دریافت شدهاست یا خیر را جواب می دهند که ناقص است. برای اینکه اطمینان حاصل کنیم درخواست مورد نظر دریافت و پردازش شدهاست، باید در منطق عملیات مورد نظر دست برده و تغییراتی را اعمال کنیم. برای این منظور فرض کنید در بستری هستیم که می توانیم از مزایای خصوصیات ACID دیتابیس رابطهای مانند SQLite استفاده کنیم. ایده به این شکل است که شناسه درخواست دریافتی را در تراکنش مشترک با عملیات اصلی ذخیره کنیم و در صورت بروز هر گونه خطا در اصل عملیات، کل تغییرات برگشت خورده و کلاینت امکان تلاش مجدد با شناسهی مورد نظر را داشته باشد. برای این منظور مدل زیر را در نظر بگیرید:
public class IdempotentId(string id, DateTime time) { public string Id { get; private init; } = id; public DateTime Time { get; private init; } = time; }
هدف از این موجودیت ثبت و نگهداری شناسههای درخواستهای دریافتی میباشد. در ادامه واسط IIdempotencyStorage را برای مدیریت نحوه ذخیره سازی و پاکسازی شناسههای دریافتی خواهیم داشت:
public interface IIdempotencyStorage { Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken); Task CleanupOutdated(CancellationToken cancellationToken); bool IsKnownException(Exception ex); }
در اینجا متد TryPersist سعی میکند با شناسه دریافتی یک رکورد را ثبت کند و اگر تکراری باشد، خروجی false خواهد داشت. متد CleanupOutdated برای پاکسازی شناسههایی که زمان مشخصی (مثلا ۱۲ ساعت) از دریافت آنها گذشته است، استفاده خواهد شد که توسط یک وظیفهی زمانبندی شده می تواند اجرا شود؛ به این صورت، امکان استفادهی مجدد از آن شناسهها برای کلاینتها مهیا خواهد شد. پیاده سازی واسط تعریف شده، به شکل زیر خواهد بود:
/// <summary> /// To prevent from race-condition, this default implementation relies on primary key constraints. /// </summary> file sealed class IdempotencyStorage( AppDbContext dbContext, TimeProvider dateTime, ILogger<IdempotencyStorage> logger) : IIdempotencyStorage { private const string ConstraintName = "PK_IdempotentId"; public Task CleanupOutdated(CancellationToken cancellationToken) { throw new NotImplementedException(); //TODO: cleanup the outdated ids based on configurable duration } public bool IsKnownException(Exception ex) { return ex is UniqueConstraintException e && e.ConstraintName.Contains(ConstraintName); } // To tackle race-condition issue, the implementation relies on storage capabilities, such as primary constraint for given IdempotentId. public async Task<bool> TryPersist(string idempotentId, CancellationToken cancellationToken) { try { dbContext.Add(new IdempotentId(idempotentId, dateTime.GetUtcNow().UtcDateTime)); await dbContext.SaveChangesAsync(cancellationToken); return true; } catch (UniqueConstraintException e) when (e.ConstraintName.Contains(ConstraintName)) { logger.LogInformation(e, "The given idempotentId [{IdempotentId}] already exists in the storage.", idempotentId); return false; } } }
همانطور که مشخص است در اینجا سعی شدهاست تا با شناسهی دریافتی، یک رکورد جدید ثبت شود که در صورت بروز خطای UniqueConstraint، خروجی با مقدار false را خروجی خواهد داد که می توان از آن نتیجه گرفت که این درخواست قبلا دریافت و پردازش شدهاست (در ادامه نحوهی استفاده از آن را خواهیم دید).
در این پیاده سازی از کتابخانه MediatR استفاده می کنیم؛ در همین راستا برای مدیریت تراکنش ها به صورت زیر می توان TransactionBehavior را پیاده سازی کرد:
internal sealed class TransactionBehavior<TRequest, TResponse>( AppDbContext dbContext, ILogger<TransactionBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IBaseCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { string commandName = typeof(TRequest).Name; await using var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.ReadCommitted, cancellationToken); TResponse? result; try { logger.LogInformation("Begin transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); result = await next(); if (result.IsError) { await transaction.RollbackAsync(cancellationToken); logger.LogInformation("Rollback transaction {TransactionId} for handling {CommandName} ({@Command}) due to failure result.", transaction.TransactionId, commandName, command); return result; } await transaction.CommitAsync(cancellationToken); logger.LogInformation("Commit transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); } catch (Exception ex) { await transaction.RollbackAsync(cancellationToken); logger.LogError(ex, "An exception occured within transaction {TransactionId} for handling {CommandName} ({@Command})", transaction.TransactionId, commandName, command); throw; } return result; } }
در اینجا مستقیما AppDbContext تزریق شده و با استفاده از خصوصیت Database آن، کار مدیریت تراکنش انجام شدهاست. همچنین باتوجه به اینکه برای مدیریت خطاها از کتابخانهی ErrorOr استفاده می کنیم و خروجی همهی Command های سیستم، حتما یک وهله از کلاس ErrorOr است که واسط IErrorOr را پیاده سازی کردهاست، یک محدودیت روی تایپ جنریک اعمال کردیم که این رفتار، فقط برروی IBaseCommand ها اجرا شود. تعریف واسط IBaseCommand به شکل زیر میباشد:
/// <summary> /// This is marker interface which is used as a constraint of behaviors. /// </summary> public interface IBaseCommand { } public interface ICommand : IBaseCommand, IRequest<ErrorOr<Unit>> { } public interface ICommand<T> : IBaseCommand, IRequest<ErrorOr<T>> { } public interface ICommandHandler<in TCommand> : IRequestHandler<TCommand, ErrorOr<Unit>> where TCommand : ICommand { Task<ErrorOr<Unit>> IRequestHandler<TCommand, ErrorOr<Unit>>.Handle(TCommand request, CancellationToken cancellationToken) { return Handle(request, cancellationToken); } new Task<ErrorOr<Unit>> Handle(TCommand command, CancellationToken cancellationToken); } public interface ICommandHandler<in TCommand, T> : IRequestHandler<TCommand, ErrorOr<T>> where TCommand : ICommand<T> { Task<ErrorOr<T>> IRequestHandler<TCommand, ErrorOr<T>>.Handle(TCommand request, CancellationToken cancellationToken) { return Handle(request, cancellationToken); } new Task<ErrorOr<T>> Handle(TCommand command, CancellationToken cancellationToken); }
در ادامه برای پیادهسازی IdempotencyBehavior و محدود کردن آن، واسط IIdempotentCommand را به شکل زیر خواهیم داشت:
/// <summary> /// This is marker interface which is used as a constraint of behaviors. /// </summary> public interface IIdempotentCommand { string IdempotentId { get; } } public abstract class IdempotentCommand : ICommand, IIdempotentCommand { public string IdempotentId { get; init; } = string.Empty; } public abstract class IdempotentCommand<T> : ICommand<T>, IIdempotentCommand { public string IdempotentId { get; init; } = string.Empty; }
در اینجا یک پراپرتی، برای نگهداری شناسهی درخواست دریافتی با نام IdempotentId در نظر گرفته شدهاست. این پراپرتی باید از طریق مقداری که از هدر درخواست HTTP دریافت میکنیم مقداردهی شود. به عنوان مثال برای ثبت کاربر جدید، به شکل زیر باید عمل کرد:
[HttpPost] public async Task<ActionResult<long>> Register( [FromBody] RegisterUserCommand command, [FromIdempotencyToken] string idempotentId, CancellationToken cancellationToken) { command.IdempotentId = idempotentId; var result = await sender.Send(command, cancellationToken); return result.ToActionResult(); }
در اینجا از همان Command به عنوان DTO ورودی استفاده شدهاست که وابسته به سطح Backward compatibility مورد نیاز، می توان از DTO مجزایی هم استفاده کرد. سپس از طریق FromIdempotencyToken سفارشی، شناسهی درخواست، دریافت شده و بر روی command مورد نظر، تنظیم شدهاست.
رفتار سفارشی IdempotencyBehavior از ۲ بخش تشکیل شدهاست؛ در قسمت اول سعی می شود، قبل از اجرای هندلر مربوط به command مورد نظر، شناسهی دریافتی را در storage تعبیه شده ثبت کند:
internal sealed class IdempotencyBehavior<TRequest, TResponse>( IIdempotencyStorage storage, ILogger<IdempotencyBehavior<TRequest, TResponse>> logger) : IPipelineBehavior<TRequest, TResponse> where TRequest : IIdempotentCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { string commandName = typeof(TRequest).Name; if (string.IsNullOrWhiteSpace(command.IdempotentId)) { logger.LogWarning( "The given command [{CommandName}] ({@Command}) marked as idempotent but has empty IdempotentId", commandName, command); return await next(); } if (await storage.TryPersist(command.IdempotentId, cancellationToken) == false) { return (dynamic)Error.Conflict( $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed."); } return await next(); } }
در اینجا IIdempotencyStorage تزریق شده و در صورتی که امکان ذخیره سازی وجود نداشته باشد، خطای Confilict که بهخطای 409 ترجمه خواهد شد، برگشت داده میشود. در غیر این صورت ادامهی عملیات اصلی باید اجرا شود. پس از آن اگر به هر دلیلی در زمان پردازش عملیات اصلی، درخواست همزمانی با همان شناسه، توسط سرور دریافت شده و پردازش شود، عملیات جاری با خطای UniqueConstaint برروی PK_IdempotentId در زمان نهایی سازی تراکنش جاری، مواجه خواهد شد. برای این منظور بخش دوم این رفتار به شکل زیر خواهد بود:
internal sealed class IdempotencyExceptionBehavior<TRequest, TResponse>(IIdempotencyStorage storage) : IPipelineBehavior<TRequest, TResponse> where TRequest : IIdempotentCommand where TResponse : IErrorOr { public async Task<TResponse> Handle( TRequest command, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(command.IdempotentId)) return await next(); string commandName = typeof(TRequest).Name; try { return await next(); } catch (Exception ex) when (storage.IsKnownException(ex)) { return (dynamic)Error.Conflict( $"The given command [{commandName}] with idempotent-id [{command.IdempotentId}] has already been received and processed."); } } }
در اینجا عملیات اصلی در بدنه try اجرا شده و در صورت بروز خطایی مرتبط با Idempotency، خروجی Confilict برگشت داده خواهد شد. باید توجه داشت که نحوه ثبت رفتارهای تعریف شده تا اینجا باید به ترتیب زیر انجام شود:
services.AddMediatR(config => { config.RegisterServicesFromAssemblyContaining(typeof(DependencyInjection)); // maintaining the order of below behaviors is crucial. config.AddOpenBehavior(typeof(LoggingBehavior<,>)); config.AddOpenBehavior(typeof(IdempotencyExceptionBehavior<,>)); config.AddOpenBehavior(typeof(TransactionBehavior<,>)); config.AddOpenBehavior(typeof(IdempotencyBehavior<,>)); });
به این ترتیب بدنه اصلی هندلرهای موجود در سیستم هیچ تغییری نخواهند داشت و به صورت ضمنی و انتخابی، امکان تعیین command هایی که نیاز است به صورت Idempotent اجرا شوند را خواهیم داشت.
References
https://www.mscharhag.com/p/rest-api-design
چرا باید میزان دسترسی به منابع یک برنامهی وب را محدود کرد؟
فرض کنید در حال ساخت یک web API هستید که کارش ذخیره سازی لیست وظایف اشخاص است و برای مثال از یک GET /api/todos برای دریافت لیست ظایف، یک POST /api/todos برای ثبت و یک PUT /api/todos/{id} برای تغییر موارد ثبت شده، تشکیل میشود.
سؤال: چه مشکلی ممکن است به همراه این سه endpoint بروز کند؟
پاسخ: به حداقل چهار مورد زیر میتوان اشاره کرد:
- یک مهاجم سعی میکند با برنامهای که تدارک دیده، هزاران وظیفهی جدید را در چند ثانیه به سمت برنامه ارسال کند تا سبب خاتمهی سرویس آن شود.
- برنامهی ما در حین سرویس دهی، به یک سرویس ثالث نیز وابستهاست و آن سرویس ثالث، اجازهی استفادهی بیش از اندازهی از منابع خود را نمیدهد. با رسیدن تعداد زیادی درخواست به برنامهی ما تنها از طرف یک کاربر، به سقف مجاز استفادهی از آن سرویس ثالث رسیدهایم و اکنون برنامه، برای تمام کاربران آن قابل استفاده نیست.
- شخصی در حال دریافت اطلاعات تک تک کاربران است. از شماره یک شروع کرده و به همین نحو جلو میرود. برای دریافت اطلاعات کاربران، نیاز است شخص به سیستم وارد شده و اعتبارسنجی شود؛ یعنی به ازای هر درخواست، یک کوئری نیز به سمت بانک اطلاعاتی جهت بررسی وضعیت فعلی و آنی کاربر ارسال میشود. به همین جهت عدم کنترل میزان دسترسی به لیست اطلاعات کاربران، بار سنگینی را به بانک اطلاعاتی و CPU سیستم وارد میکند.
- هم اکنون چندین موتور جستجو و باتهایی نظر آنها در حال پیمایش سایت و برنامهی شما هستند که هر کدام از آنها میتوانند در حد یک مهاجم رفتار کنند.
به صورت خلاصه، همیشه استفادهی از برنامه، به آن نحوی که ما پیشبینی کردهایم، به پیش نمیرود و در آن لحظه، برنامه، در حال استفاده از CPU، حافظه و بانک اطلاعاتی به اشتراک گذاشته شدهی با تمام کاربران برنامهاست. در این حالت فقط یک کاربر مهاجم میتواند سبب از کار افتادن و یا به شدت کند شدن این برنامه شود و دسترسی سایر کاربران همزمان را مختل کند.
محدود کردن نرخ دسترسی به برنامه چیست؟
Rate limiting و یا نام دیگر آن request throttling، روشی است که توسط آن بتوان از الگوهای پیش بینی نشدهی استفادهی از برنامه جلوگیری کرد. عموما برنامههای وب، محدود کردن نرخ دسترسی را بر اساس تعداد بار درخواست انجام شدهی در یک بازهی زمانی مشخص، انجام میدهند و یا اگر کار برنامهی شما ارائهی فیلمهای ویدیویی است، شاید بخواهید میزان حجم استفاده شدهی توسط یک کاربر را کنترل کنید. در کل هدف نهایی از آن، کاهش و به حداقل رساندن روشهای آسیب زنندهی به برنامه و سیستم است؛ صرفنظر از اینکه این نحوهی استفادهی خاص، سهوی و یا عمدی باشد.
محدود کردن نرخ دسترسی را باید به چه منابعی اعمال کرد؟
پاسخ دقیق به این سؤال: «همه چیز» است! بله! همه چیز را کنترل کنید! در اینجا منظور از همه چیز، همان endpointهایی هستند که استفادهی نابجای از آنها میتوانند سبب کند شدن برنامه یا از دسترس خارج شدن آن شوند. برای مثال هر endpointای که از CPU، حافظه، دسترسی به دیسک سخت، بانک اطلاعاتی، APIهای ثالث و خارجی و امثال آن استفاده میکند، باید کنترل و محدود شود تا استفادهی ناصحیح یک کاربر از آنها، استفادهی از برنامه را برای سایر کاربران غیرممکن نکند. البته باید دقت داشت که هدف از اینکار، عصبی کردن کاربران عادی و معمولی برنامه نیست. هدف اصلی در اینجا، تشویق به استفادهی منصفانه از منابع سیستم است.
الگوریتمهای محدود کردن نرخ دسترسی
پیاده سازی ابتدایی محدود کردن نرخ دسترسی به منابع یک برنامه کار مشکلی است و در صورت استفاده از الگوریتمهای متداولی مانند تعریف یک جدول که شامل user-id، action-id و timestamp، به همراه یکبار ثبت اطلاعات به ازای هر درخواست و همچنین خواندن اطلاعات موجود است که جدول آن نیز به سرعت افزایش حجم میدهد. به همین جهت تعدادی الگوریتم بهینه برای اینکار طراحی شدهاند:
الگوریتمهای بازهی زمانی مشخص
در این روش، یک شمارشگر در یک بازهی زمانی مشخص فعال میشود و بر این مبنا است که محدودیتها اعمال خواهند شد. یک مثال آن، مجاز دانستن فقط «100 درخواست در یک دقیقه» است که نام دیگر آن «Quantized buckets / Fixed window limit» نیز هست.
برای مثال «نام هر اکشن + یک بازهی زمانی»، یک کلید دیکشنری نگهدارندهی اطلاعات محدود کردن نرخ دسترسی خواهد بود که به آن کلید، «bucket name» هم میگویند؛ مانند مقدار someaction_106062120. سپس به ازای هر درخواست رسیده، شمارشگر مرتبط با این کلید، یک واحد افزایش پیدا میکند و محدود کردن دسترسیها بر اساس مقدار این کلید صورت میگیرد. در ادامه با شروع هر بازهی زمانی جدید که در اینجا window نام دارد، یک کلید یا همان «bucket name» جدید تولید شده و مقدار متناظر با این کلید، به صفر تنظیم میشود.
اگر بجای دیکشنریهای #C از بانک اطلاعاتی Redis برای نگهداری این key/valueها استفاده شود، میتوان برای هر کدام از مقادیر آن، طول عمری را نیز مشخص کرد تا خود Redis، کار حذف خودکار اطلاعات غیرضروری را انجام دهد.
یک مشکل الگوریتمهای بازهی زمانی مشخص، غیر دقیق بودن آنها است. برای مثال فرض کنید که به ازای هر 10 ثانیه میخواهید تنها اجازهی پردازش 4 درخواست رسیده را بدهید. مشکل اینجا است که در این حالت یک کاربر میتواند 5 درخواست متوالی را بدون مشکل ارسال کند؛ 3 درخواست را در انتهای بازهی اول و دو درخواست را در ابتدای بازهی دوم:
به یک بازهی زمانی مشخص، fixed window و به انتها و ابتدای دو بازهی زمانی مشخص متوالی، sliding window میگویند. همانطور که در تصویر فوق هم مشاهده میکنید، در این اگوریتم، امکان محدود سازی دقیقی تنها در یک fixed window میسر است و نه در یک sliding window.
سؤال: آیا این مساله عدم دقت الگوریتمهای بازهی زمانی مشخص مهم است؟
پاسخ: بستگی دارد! اگر هدف شما، جلوگیری از استفادهی سهوی یا عمدی بیش از حد از منابع سیستم است، این مساله مشکل مهمی را ایجاد نمیکند. اما اگر دقت بالایی را انتظار دارید، بله، مهم است! در این حالت از الگوریتمهای «sliding window limit » بیشتر استفاده میشود که در پشت صحنه از همان روش استفادهی از چندین fixed window کوچک، کمک میگیرند.
الگوریتمهای سطل توکنها (Token buckets)
در دنیای مخابرات، از الگوریتمهای token buckets جهت کنترل میزان مصرف پهنای باند، زیاد استفاده میشود. از واژهی سطل در اینجا استفاده شده، چون عموما به همراه آب بکارگرفته میشود:
فرض کنید سطل آبی را دارید که در کف آن نشتی دارد. اگر نرخ پر کردن این سطل، با آب، از نرخ نشتی کف آن بیشتر باشد، آب از سطل، سرریز خواهد شد. به این معنا که با سرریز توکنها یا آب در این مثال، هیچ درخواست جدید دیگری پردازش نمیشود؛ تا زمانیکه مجددا سطل، به اندازهای خالی شود که بتواند توکن یا آب بیشتری را بپذیرد.
یکی از مزیتهای این روش، نداشتن مشکل عدم دقت به همراه بازههای زمانی مشخص است. در اینجا اگر تعداد درخواست زیادی به یکباره به سمت برنامه ارسال شوند، سطل پردازشی آنها سرریز شده و دیگر پردازش نمیشوند.
مزیت دیگر آنها، امکان بروز انفجاری یک ترافیک (bursts in traffic) نیز هست. برای مثال اگر قرار است سطلی با 60 توکن در دقیقه پر شود و این سطل نیز هر ثانیه یکبار تخلیه میشود، کلاینتها هنوز میتوانند 60 درخواست را در طی یک ثانیه ارسال کنند (ترافیک انفجاری) و پس از آن نرخ پردازشی، یک درخواست به ازای هر ثانیه خواهد شد.
آیا باید امکان بروز انفجار در ترافیک را داد؟
عموما در اکثر برنامهها وجود یک محدود کنندهی نرخ دسترسی کافی است. برای مثال یک محدود کنندهی نرخ دسترسی سراسری 600 درخواست در هر دقیقه، برای هر endpoint ای شاید مناسب باشد. اما گاهی از اوقات نیاز است تا امکان بروز انفجار در ترافیک (bursts) را نیز درنظر گرفت. برای مثال زمانیکه یک برنامهی موبایل شروع به کار میکند، در ابتدای راه اندازی آن تعداد زیادی درخواست، به سمت سرور ارسال میشوند و پس از آن، این سرعت کاهش پیدا میکند. در این حالت بهتر است چندین محدودیت را تعریف کرد: برای مثال امکان ارسال 10 درخواست در هر ثانیه و حداکثر 3600 درخواست در هر ساعت.
روش تشخیص کلاینتها چگونه باشد؟
تا اینجا در مورد bucket name یا کلید دیکشنری اطلاعات محدود کردن دسترسی به منابع، از روش «نام هر اکشن + یک بازهی زمانی» استفاده کردیم. به این کار «پارتیشن بندی درخواستها» هم گفته میشود. روشهای دیگری نیز برای انجام اینکار وجود دارند:
پارتیشن بندی به ازای هر
- endpoint
- آدرس IP. البته باید دقت داشت که کاربرانی که در پشت یک پروکسی قرار دارند، از یک IP آدرس اشتراکی استفاده میکنند.
- شماره کاربری. البته باید در اینجا بحث کاربران اعتبارسنجی نشده و anonymous را نیز مدنظر قرار داد.
- شمار سشن کاربر. در این حالت باید بحث ایجاد سشنهای جدید به ازای دستگاههای مختلف مورد استفادهی توسط کاربر را هم مدنظر قرار داد.
- نوع مروگر.
- هدر ویژه رسیده مانند X-Api-Token
بسته به نوع برنامه عموما از ترکیبی از موارد فوق برای پارتیشن بندی درخواستهای رسیده استفاده میشود.
درنظر گرفتن حالتهای استثنائی
هرچند همانطور که عنوان شد تمام قسمتهای برنامه باید از لحاظ میزان دسترسی محدود شوند، اما استثناءهای زیر را نیز باید درنظر گرفت:
- عموما تیم مدیریتی یا فروش برنامه، بیش از سایر کاربران، با برنامه کار میکنند.
- بیش از اندازه محدود کردن Web crawlers میتواند سبب کاهش امتیاز SEO سایت شما شود.
- گروههای خاصی از کاربران برنامه نیز میتوانند دسترسیهای بیشتری را خریداری کنند.
نحوهی خاتمهی اتصال و درخواست
اگر کاربری به حد نهایی استفادهی از منابع خود رسید، چه باید کرد؟ آیا باید صرفا درخواست او را برگشت زد یا اطلاعات بهتری را به او نمایش داد؟
برای مثال GitHub یک چنین خروجی را به همراه هدرهای ویژهای جهت مشخص سازی وضعیت محدود سازی دسترسی به منابع و علت آن، ارائه میدهد:
> HTTP/2 403 > Date: Tue, 20 Aug 2013 14:50:41 GMT > x-ratelimit-limit: 60 > x-ratelimit-remaining: 0 > x-ratelimit-used: 60 > x-ratelimit-reset: 1377013266 > { > "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)", > "documentation_url": "https://docs.github.com/rest/overview/resources-in-the-rest-api#rate-limiting" > }
حتی یکسری از APIها از status codeهای ویژهای مانند 403 (دسترسی ممنوع)، 503 (سرویس در دسترس نیست) و یا 429 (تعداد درخواستهای زیاد) برای پاسخ دهی استفاده میکنند.
محل ذخیره سازی اطلاعات محدود سازی دسترسی به منابع کجا باشد؟
اگر محدودسازی دسترسی به منابع، جزئی از مدل تجاری برنامهی شما است، نیاز است حتما از یک بانک اطلاعاتی توزیع شده مانند Redis استفاده کرد تا بتواند اطلاعات تمام نمونههای در حال اجرای برنامه را پوشش دهد. اما اگر هدف از این محدود سازی تنها میسر ساختن دسترسی منصفانهی به منابع آن است، ذخیره سازی آنها در حافظهی همان نمونهی در حال اجرای برنامه هم کافی است.
namespace jqGrid10.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string CategoryName { set; get; } public int NumberOfViews { set; get; } } }
- گروه بندی بر روی ستون CategoryName انجام شود.
- ستونی که بر روی آن گروه بندی انجام میشود، نمایش داده نشود.
- در ابتدای نمایش گروهها، تمام آنها به صورت جمع شده و Collapsed نمایش داده شوند.
- پس از نمایش گروهها، اولین گروه به صورت خودکار باز شود.
- تعداد ردیف هر گروه به عنوان گروه اضافه شود.
- جمع کل ستون تعداد بار مشاهدات هر گروه قابل محاسبه شود.
- جمع کل هر گروه در زمانیکه هر گروه نیز بستهاست نمایش داده شود.
- رنگ ردیف جمع کل قابل تنظیم باشد.
فعال سازی گروه بندی در jqGrid
فعال سازی گروه بندی در jqGrid به سادگی افزودن تعاریف ذیل است:
$('#list').jqGrid({ caption: "آزمایش دهم", //... grouping: true, groupingView: { groupField: ['CategoryName'], groupOrder: ['asc'], groupText : ['<b>{0} - {1} ردیف</b>'], groupDataSorted: true, groupColumnShow: false, groupCollapse: true, groupSummary: [true], showSummaryOnHide: true } });
groupField بیانگر آرایهای از ستونهایی است که قرار است بر روی آنها گروه بندی صورت گیرد.
groupOrder آرایهای اختیاری از مقادیر asc یا desc است که متناظر هستند با نحوهی مرتب سازی پیش فرض ستونهای معرفی شده در آرایه groupField.
groupText آرایهای اختیاری از عناوین گروه بندیهای انجام شدهاست. اگر ذکر شود، {0} آن با نام گروه و {1} آن با تعداد عناصر گروه جایگزین میشود.
تنظیم groupDataSorted سبب خواهد شد تا نام ستونی که بر روی آن گروه بندی صورت میگیرد، به سرور ارسال شود (توسط پارامتر sidx). به این ترتیب در سمت سرور میتوان اطلاعات را به صورت پویا مرتب سازی کرده و بازگشت داد.
با تنظیم groupColumnShow به false سبب خواهیم شد تا ستونهای معرفی شده در قسمت groupField نمایش داده نشوند.
با تنظیم groupCollapse به true، در ابتدای نمایش گروهها، ردیفهای آنها نمایش داده نخواهند شد و در حالت جمع شده قرار میگیرند.
groupSummary به معنای فعال سازی نمایش ردیف محاسبهی summary مانند sum، min، max و امثال آن بر روی یک گروه است.
اگر مقدار showSummaryOnHide مساوی true باشد، ردیف محاسبهی summary حتی در حالت groupCollapse: true نمایش داده خواهد شد.
فعال سازی محاسبهی جمع ستون تعداد بار مشاهدات مطالب
برای فعال سازی نهایی محاسبهی جمع ستون تعداد بار مشاهدات، علاوه بر تنظیم groupSummary به true، نیاز است در همان ستون مشخص کنیم که این محاسبات چگونه باید انجام شوند:
colModel: [ // ........ { name: '@(StronglyTyped.PropertyName<Post>(x => x.Title))', index: '@(StronglyTyped.PropertyName<Post>(x => x.Title))', align: 'right', width: 150, summaryTpl: '<div style="text-align: left;">خلاصه </div>', summaryType: function (val, name, record) { return ""; } }, // ........ { name: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))', index: '@(StronglyTyped.PropertyName<Post>(x => x.NumberOfViews))', align: 'center', width: 70, summaryType: 'sum', summaryTpl: '<b>جمع مشاهدات: {0}</b>' } ],
summaryType مانند ستون عنوان، سفارشی شده نیز میتوان باشد. در ردیف summary و ستون عنوان تنها میخواهیم یک مقدار ثابت را نمایش دهیم، به همین جهت summaryType آن به یک مقدار خالی تنظیم شدهاست.
تغییر رنگ ردیف خلاصه عملیات هر گروه به همراه گشودن خودکار اولین گروه
گروه بندی به همراه یک سری متد توکار نیز هست. برای مثال اگر متد groupingToggle را بر روی Id هر گروه فراخوانی کنیم، میتوان سبب باز یا بسته شده آن گروه شد. متدهای دیگری مانند groupingGroupBy برای گروه بندی پویا و groupingRemove برای حذف گروه بندی نیز وجود دارند:
$('#list').jqGrid({ caption: "آزمایش دهم", //......... loadComplete: function() { //......... $('#list').jqGrid('groupingToggle', 'list' + 'ghead_0_0'); $("tr.jqfoot td").css({ "background": "#2f4f4f", "color": "#FFF" }); }, });
مثال کامل این قسمت را از اینجا میتوانید دریافت کنید:
jqGrid10.zip