سلیب پس از برگزاری موفقیتآمیز [**مجموعه مسابقات کداستار**](https://quera.org/events/codestar-0403)، دوباره با **ممزواد** *(Mamzavad)،* **عنصر فتنهی کوئرا** به مشکلات جدیای بر میخورد و از آنجایی که **قدرت ممزواد** در کوئرا **خیلی بیشتر از سلیب** است، او ناچار میشود تا برای اتمام درگیری در کوئرا، پس از اتمام مسابقات کداستار به **مزرعه پنبهاش** پناه برده و **به کشت و برداشت پنبه مشغول شود!**
از آنجایی که سلیب علاوه بر فردی **بسیار بانظم** بودن، به **اسکریپتهای** *Lua* و همچنین **ردیس کلاستر** *(Redis Cluster)* بسیار علاقهمند است، از شما که تنها فرد مورد اعتماد باقی مانده برای او هستید، میخواهد تا مدیریت **کشت** و **برداشت پنبه** در مزرعهاش را با استفاده از **دو** *Endpoint*، `/get` و `/set` که توسط یک [**سرویس** *OpenResty*](https://openresty.org/en/) و [**اسکریپتهای** *Lua*](https://www.lua.org/) پیادهسازی خواهند شد، مدیریت کنید. وظیفه شما در این سوال پیادهسازی بخشی از این سیستم مدیریت پنبههای سلیب با استفاده از **اسکریپتهای** *Lua*، کانفیگ **فایل** `nginx.conf` و نوشتن **فایل** `docker-compose.yml` برای **راهاندازی سرویسهای مورد نیاز مزرعه پنبه** میباشد.

**پنبههای سلیب** به صورت مقادیر **کلید-مقدار** هستند که شما باید آنها را در **یک ردیس کلاستر با ۳ عدد نود** *(Node)* ذخیرهسازی کنید. سلیب میخواهد تا برای **بالانس لود در نودهای ردیسی،** هر بار **کلیدهای پنبههایش** را با استفاده از **الگوریتم هش** *FNV-1a* هش کرده و به یکی از **شاردهای** `shard1`, `shard2`, `shard3` اختصاص دهد که **هر کدام از ۳ نود ردیسی** *(یا Replica آنها)* میباشد.
# **پروژه اولیه**
برای دانلود **پروژهی اولیه** روی [این لینک](/contest/assignments/85281/download_problem_initial_project/291403/) کلیک کنید.
<details class="yellow">
<summary>
**ساختار فایلها**
</summary>
```
├── <mark class="red" title="این فایل باید پیادهسازی شود">docker-compose.yml</mark>
├── nginx
│ ├── <mark class="red" title="این پوشه باید شامل کدهای منطقی این سوال باشد">lua</mark>
│ └── <mark class="red" title="این فایل باید پیادهسازی شود">nginx.conf</mark>
```
</details>
برای راهاندازی پروژه، از یک **فایل** `docker-compose.yml` استفاده خواهد شد، که شامل چند سرویس مختلف برای **مدیریت دادههای پنبههای مزرعه سلیب** با استفاده از *Redis* و *OpenResty* میباشد.
# **جزئیات پروژه**
<details class="red">
<summary>
**پیادهسازی فایل کانفیگ** `nginx.conf`**و اسکریپتهای** *Lua*، **منطق مزرعه پنبه!**
</summary>
در این سوال، شما وظیفه دارید تا با استفاده از *OpenResty،* که یک سیستم مبتنی بر *Nginx* با پشتیبانی از *Lua* پیادهسازی کنید. این سیستم باید شامل **پیکربندی** کامل سرور *Nginx* در مسیر `nginx/nginx.conf` و همچنین **اسکریپتهای** *Lua* در مسیر پوشه `nginx/lua` باشد که **منطق مورد نیاز برای مدیریت درخواستها و ارتباط با کلاستر ردیسی را اجرا میکنند.**
+ لازم است فایل پیکربندی `nginx.conf` را بهصورت کامل بنویسید. این فایل باید شامل تنظیمات مربوط به بارگذاری *Lua،* تعریف مسیرهای مورد نیاز (مانند `/get` و `/set`)، و پیکربندی منابع اشتراکی (مثل `lua_shared_dict`) باشد. توجه داشته باشید که **تنها دو مسیر** *(Route)* `get` و `set` مجاز خواهند بود و *Nginx* باید به ازای باقی مسیرها خطای `404` نمایش دهد.
+ **اسکریپتهای** مربوط به منطق **برنامهنویسی** باید در مسیر `nginx/lua/` قرار داده شوند. **این اسکریپتها ممکن است شامل ماژولهایی برای موارد زیر باشند، البته توجه داشته باشید که مدیریت چگونگی نحوه پیادهسازی اجزای مختلف بر عهدهی شما میباشد:**
+ **اتصال به کلاستر ردیس** *(شامل مدیریت Shard، Fallback و Health check)*
+ **اعمال محدودیت نرخ** *(Rate limiting)*
+ **خواندن و نوشتن کلیدها با استفاده از** *GET* **و** *SET*
+ **تولید پاسخ در قالب** *JSON* **و هندل کردن خطاها**
ساختار پروژه شما باید به گونهای باشد که امکان **اجرای کامل سیستم** با راهاندازی *Nginx* فراهم شود. **توجه داشته باشید** که **تمامی منطق سمت سرور** باید به کمک *Lua* و از طریق *OpenResty* اجرا شود و **اجازه استفاده از سرورهای خارجی یا زبانهای دیگر وجود ندارد.**
**در ادامه به بررسی هر کدام از موارد ذکر شده به صورت مفصلتر همراه با مثال خواهیم پرداخت.**
</details>
<details class="blue">
<summary>
**پیادهسازی فایل** `docker-compose.yml`، **زیرساخت مزرعه پنبه!**
</summary>
## **سرویس** *OpenResty*
با استفاده از **ایمیج** زیر یک **سرویس** با نام `openresty` ایجاد کنید:
```
registry.gitlab.com/qio/standard/openresty:1.19.9.1-5-alpine-fat
```
+ این **سرویس** برای اجرای **کدهای** *Lua* و مدیریت درخواستها با استفاده از *Nginx* و *OpenResty* استفاده خواهد شد.
+ **پورت** `8080` باید از **میزبان** به **کانتینر** متصل شود.
+ **فایل کانفیگ** *Nginx* در **مسیر** `nginx/nginx.conf` و اسکریپتهای پیادهسازی شده در **پوشه** `nginx/lua` را به **مسیرهای مرتبط** در **سرویس** `openresty` متصل کنید.
+ **این سرویس به تمامی نودهای کلاستر ردیسی وابسته است!**
## **سرویسهای** *Redis*
**مجموعاً ۶ سرویس** *Redis* در این پروژه تعریف شدهاند:
+ **سه نود** *Redis* **اصلی** (`redis1`, `redis2`, `redis3`)
+ **سه نود** *Redis* به عنوان *Replica* (`redis1r`, `redis2r`, `redis3r`)
همهی این سرویسها از ایمیج زیر استفاده میکنند:
```
registry.gitlab.com/qio/standard/redis:latest
```
+ این سرویسها برای **ذخیرهسازی دادهها** در یک **ساختار توزیعشده** *Redis* استفاده میشوند.
# **نکاتی در مورد پیادهسازی فایل** `docker-compose.yml`
+ **توجه:** شما اجازهی *build* کردن یک ایمیج جدید **نخواهید داشت** و باید از ایمیجهای استاندارد کوئرا استفاده کنید.
+ **توجه:** **نام کانتینرها و سرویسها** باید **دقیقا** عبارات گفته شده در متن سوال باشند.
+ **توجه:** سیستم داوری کوئرا بهصورت خودکار فایل `docker-compose.yml` را با کامند `up` اجرا میکند. **شما نیازی به کد یا اسکریپتی برای اجرای این کار ندارید.**
+ **توجه:** ورژن `docker-compose.yml` باید `3.3` باشد.
+ **توجه:** تنها فایل `docker-compose.yml` شما در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتاثیر** خواهند بود. همچنین شما باید حتما **تنها** از **ایمیجهای معرفی شده در متن سوال** استفاده کنید. **استفاده از سایر ایمیجها باعث عدم داوری ارسال شما خواهد شد.**
``` yaml docker-compose.yml docker
version: "3.3"
# Do not forget that the only available
# openresty image is accessible with the following url:
# registry.gitlab.com/qio/standard/openresty:1.19.9.1-5-alpine-fat
# Do not forget that the only available
# redis image is accessible with the following url:
# registry.gitlab.com/qio/standard/redis:latest
```
</details>
<details class="green">
<summary>
**پیادهسازی** *Setter* **برای کشت پنبه!**
</summary>
در این بخش، **سلیب** قرار است تا امکان **ذخیره مقدار** *(Value)* **جدید پنبه** برای یک **کلید** *(Key)* دلخواه را فراهم کند. ابتدا بررسی میشود آیا **کلید** (`key`) و **مقدار** (`value`) در *Query String* درخواست وجود دارد یا خیر. سپس با استفاده از تابع هش گفته شده، مشخص میشود کلید در کدام **شارد** *(Shard)* **کلاستر ردیسی** *(Redis Cluster)* قرار دارد. شارد هر **کلید** $key$ به این صورت مشخص خواهد شد:
$$FNV-1a(key) mod 3 + 1$$
- **طبق رابطه بالا**، پس از محاسبه هش کلید `key` **باقیمانده آن را بر ۳** محاسبه کرده و **یکی شیفت میدهیم** تا **شارد** *(Shard)* نهاییاش محاسبه شود.
پس از محاسبه شارد کلید مورد نظر و افزودن مقدار آن کلید در نود مورد مشخص شده (یا Replica ی آن)، در صورتی که این ذخیرهسازی موفق بود، خروجی باید یک *JSON* شامل وضعیت (`status`) **موفقیت** (`OK`)، **کلید** (`key`)، **مقدار** (`value`) ذخیرهشده و **نام شارد** (`shard`) باشد.
اگر **مقدار** یا **کلید** ارسال نشده باشند، باید **خطایی** با کد `400` به همراه پیام مناسب نمایش داده شود. اگر عملیات `SET` شکست بخورد *(مثلاً ردیسی در دسترس نباشد)،* باید **خطای** `500` **به همراه پیام مناسب بازگردانده میشود.**
همانطور که پیشتر توضیح داده شد، در مسیر `/set`، **درخواستهایی** برای **ثبت مقدار** (`value`) برای **یک کلید** (`key`) خاص ارسال میشوند. این بخش به طور کلی شامل مراحل زیر است:
## **مراحل پیادهسازی**
- **بررسی وجود** `key` و `value` در *Query String.*
- محاسبه هش *FNV-1a* روی `key` برای تعیین **شارد کلید** (یکی از `shard1`, `shard2`, `shard3`).
- تلاش برای اتصال به **نود اصلی** *(Primary Node)* ردیس مربوطه.
- تلاش برای **اتصال** به نود *Replica* ی شارد مورد نظر در صورت در دسترس نبود **نود اصلی**، مثلا در صورتی که **شارد کلیدی** `shard1` باشد، **سرویس** *OpenResty* باید سعی کند تا به **نود ردیسی** `redis1` متصل شود و **در صورت شکست**، به **نود** *Replica* ی آن یعنی `redis1r` متصل شود.
- ذخیرهسازی **مقدار** مورد نظر برای **کلید** در **دیتابیس ردیسی**
- بازگرداندن *JSON* با کد `200` شامل اطلاعات `key`, `value`, `shard`, `status`.
## مثالها
### **درخواستهای موفق برای ثبت مقدار جدید**
```bash terminal terminal
curl "http://localhost:8080/set?key=key3&value=value3"
```
```json output json
{
"key": "key3",
"value": "value3",
"status": "OK",
"shard": "shard1"
}
```
```bash terminal terminal
curl "http://localhost:8080/set?key=key1&value=value1"
```
```json output json
{
"key": "key2",
"value": "value2",
"status": "OK",
"shard": "shard2"
}
```
```bash terminal terminal
curl "http://localhost:8080/set?key=key6&value=value6"
```
```json output json
{
"key": "key6",
"value": "value6",
"status": "OK",
"shard": "shard3"
}
```
### **درخواستهای ناموفق برای ثبت مقدار جدید، بدون کلید و مقدار**
```bash terminal terminal
curl "http://localhost:8080/set?value=blue"
```
```json output json
{
"error": "Missing 'key'"
}
```
```bash terminal terminal
curl "http://localhost:8080/set?key=color"
```
```json output json
{
"error": "Missing 'value'"
}
```
### **درخواست ناموفق برای ثبت مقدار جدید، در دسترس نبودن همزمان نود اصلی و** *Replica*
```bash terminal terminal
curl "http://localhost:8080/set?key=color&value=green"
```
```json output json
{
"error": "Redis unavailable"
}
```
| توضیحات | کد وضعیت | خروجی نمونه |
|----------------------------------|----------|------------------------------------------------------------------------------|
| **ارسال درست مقدار و کلید** | `200` | `{ "key": "key3", "value": "value3", "status": "OK", "shard": "shard1" }` |
| **ارسال بدون** key | `400` | `{ "error": "Missing 'key'" }` |
| **ارسال بدون** value | `400` | `{ "error": "Missing 'value'" }` |
| **عدم دسترسی به نود اصلی** و `replica` | `500` | `{ "error": "Redis unavailable" }` |
</details>
<details class="yellow">
<summary>
**پیادهسازی** *Getter* **برای برداشت پنبه!**
</summary>
در این بخش، **سلیب** باید قادر باشد **مقدار ذخیرهشدهای** را با استفاده از **کلید** (`key`) از کلاستر **ردیس** دریافت کند. ابتدا بررسی میشود آیا **کلید** در *Query String* درخواست وجود دارد یا خیر. سپس با استفاده از تابع هش گفته شده، مشخص میشود این کلید در کدام **شارد** *(Shard)* از **کلاستر ردیسی** قرار دارد. برای تعیین شارد، از رابطه زیر استفاده میشود:
$$FNV-1a(key) mod 3 + 1$$
- پس از محاسبه هش `key`، **باقیمانده تقسیم بر ۳** محاسبه شده و با **۱ جمع** میشود تا شماره شارد (`shard1`, `shard2`, `shard3`) مشخص گردد.
- تلاش میشود تا به **نود اصلی** *(Primary)* **شارد** مربوطه متصل شویم.
- اگر نود اصلی در دسترس **نبود**، اتصال به *Replica* صورت میگیرد.
- سپس دستور `GET` روی کلید اجرا میشود.
- اگر **مقدار** یافت نشد (`null`)، فیلد `found` باید مقدار `false` داشته باشد.
- در صورت **موفقیت**، **خروجی شامل مقادیر** `key`, `value`, `found`, `shard` در **قالب** *JSON* و **کد وضعیت** `200` خواهد بود.
- در صورت **ارسال نشدن کلید**، **کد خطای** `400` با **پیام مناسب** بازگردانده میشود.
- در صورت **عدم در دسترس بودن هیچ نودی**، **کد** `500` به همراه **پیام مناسب** بازگردانده میشود.
## **مراحل پیادهسازی**
- **بررسی وجود** `key` در *Query String.*
- محاسبه **هش** *FNV-1a* روی `key` برای تعیین **شارد کلید**
- تلاش برای اتصال به **نود اصلی** شارد مربوطه.
- تلاش برای اتصال به *Replica* در صورت **عدم موفقیت** در اتصال به **نود اصلی.**
- **دریافت مقدار کلید از دیتابیس ردیسی**
- **بازگرداندن** نتیجه در قالب *JSON* همراه با **وضعیت** `200`.
## **مثالها**
### **درخواستهای موفق برای دریافت مقدار کلید**
```bash terminal terminal
curl "http://localhost:8080/get?key=key1"
```
```json output json
{
"key": "key1",
"found": true,
"value": "value1",
"shard": "shard2"
}
```
```bash terminal terminal
curl "http://localhost:8080/get?key=key6"
```
```json output json
{
"key": "key6",
"found": true,
"value": "value6",
"shard": "shard3"
}
```
### **درخواست ناموفق بدون کلید**
```bash terminal terminal
curl "http://localhost:8080/get"
```
```json output json
{
"error": "Missing 'key'"
}
```
### **درخواست برای کلید ناموجود**
```bash terminal terminal
curl "http://localhost:8080/get?key=unknown"
```
```json output json
{
"key": "unknown",
"found": false,
"value": null,
"shard": "shard2"
}
```
### **درخواست ناموفق به دلیل عدم دسترسی به ردیس**
```bash terminal terminal
curl "http://localhost:8080/get?key=key9"
```
```json output json
{
"error": "Redis unavailable"
}
```
| توضیحات | کد وضعیت | خروجی نمونه |
|----------------------------------|----------|------------------------------------------------------------------------------|
| **دریافت مقدار موجود** | `200` | `{ "key": "key1", "found": true, "value": "value1", "shard": "shard2" }` |
| **عدم ارسال کلید** | `400` | `{ "error": "Missing 'key'" }` |
| **دریافت کلیدی که وجود ندارد** | `200` | `{ "key": "unknown", "found": false, "value": null, "shard": "shard3" }` |
| **عدم دسترسی به نود اصلی** و `replica` | `500` | `{ "error": "Redis unavailable" }` |
</details>
<details class="blue">
<summary>
**پیادهسازی** *Rate Limiter*، **برای محافظت مزرعه!**
</summary>
برای محافظت از **منابع سیستم** در برابر حملات و بار بیش از حد، سلیب باید یک **محدودکننده نرخ درخواست** (*Rate Limiter*) بر پایهی **IP کلاینت** پیادهسازی کند.
## **مراحل پیادهسازی**
- در هر بازه **۶۰ ثانیهای**، **حداکثر ۶۰ درخواست** از یک *IP* **مجاز** است.
- **اگر تعداد خطاها از این حد بیشتر شود**، کاربر باید با **خطای** `429 Too Many Requests` مواجه شده و از ادامه فعالیت او برای **مدت زمان ۱۵ ثانیهای،** جلوگیری به عمل آید.
## مثالها
### **درخواست بیش از حد مجاز**
```bash terminal terminal
curl "http://localhost:8080/get?key=key1"
```
```json output json
{
"error": "Rate limit exceeded"
}
```
| توضیحات | کد وضعیت | خروجی نمونه |
|----------------------------------|----------|------------------------------------------------------------------------------|
| **درخواست در محدودۀ مجاز** | `200` | `{ "key": "test", "value": "val", "status": "OK", "shard": "shard1" }` |
| **عبور از حد مجاز درخواست** | `429` | `{ "error": "Rate limit exceeded" }` |
</details>
<details class="pink">
<summary>
**پیادهسازی** *Fallback*، **برای پشتیبانی مزرعه!**
</summary>
برای اطمینان از **پایداری بالا** و جلوگیری از قطع خدمات در زمان از کار افتادن برخی نودهای ردیسی، سلیب باید از **مکانیزم** *(Fallback)* استفاده کند.
* در زمان **پردازش درخواست**، اگر **نود اصلی** سالم باشد، به آن متصل میشویم. در غیر اینصورت، به **نود** *Replica* متصل میشویم.
* اگر **هیچکدام** در دسترس **نبودند**، پاسخ با **خطای** `500` و **پیام** `Redis unavailable` بازگردانده میشود.
## **مراحل پیادهسازی**
- **بررسی سلامت و دسترس پذیری نود اصلی**
- **اتصال** به **نود اصلی** در صورت سالم بودن
- **اتصال** به نود *Replica* در صورت ناسالم بودن نود اصلی.
- **در صورت در دسترس نبودن هر دو**، بازگرداندن **خطای** `500`.
## مثالها
### **عدم دسترسی به نود اصلی و Replica**
```bash terminal terminal
curl "http://localhost:8080/get?key=key4"
```
```json output json
{
"error": "Redis unavailable"
}
```
| توضیحات | کد وضعیت | خروجی نمونه |
|----------------------------------|----------|------------------------------------------------------------------------------|
| **نود اصلی سالم و قابل اتصال** | `200` | `{ "key": "key5", "found": true, "value": "value5", "shard": "shard3" }` |
| **اتصال مستقیم به** `replica` | `200` | `{ "key": "key6", "found": true, "value": "value6", "shard": "shard2" }` |
| **عدم دسترسی به هر دو نود** | `500` | `{ "error": "Redis unavailable" }` |
</details>
# آنچه باید آپلود کنید
+ **توجه**: پس از اعمال تغییرات، کل پروژه را _Zip_ کرده و آپلود کنید. **همانند پروژه اولیه در فایل زیپ شده نباید کد در پوشهی دیگری قرار بگیرد در غیر این صورت سیستم داوری فایل را شناسایی نکرده و نمرهای دریافت نخواهید کرد.**
+ **توجه:** تنها فایلهایی که در **ساختار پروژه** مشخص شدهاند، در سیستم داوری **مورد پذیرش** قرار خواهد گرفت و سایر تغییرات در سایر فایلها **بیتاثیر** خواهند بود.