Gunicorn یا Daphne?

Gunicorn و Daphne هر دو وب سرور برای فریمورکهای پایتون هستن که روی لینوکس نصب میشن.

فرقشون چیه؟

Gunicorn برای درخواست های Syncronous و HTTP request ها استفاده میشه. تنظیمات WSGI باید ست شه.

Daphne برای درخواست های Asynchronous و Websocket Request ها کاربرد داره و تنظیمات ASGI باید روشون انجام شه.

از کدوم استفاده کنیم؟

از Daphne میشه برای هر دو استفاده کرد. اگر درخواست های Async ندارید میتونید از Gunicorn استفاده کنید. یه راه حل دیگه هم برای سولوشنهای بزرگ اینه که Nginx رو استفاده کنید و تنظیم کنید که درخواست های Websocker رو به Daphne و درخواست های HTTP رو به Gunicorn بفرسته.

وب سوکت WebSocket چیه

WebSocket یه پروتکل وبه که امکان ارسال پیام دو طرفه بین کلاینت و سرور رو میده.

توی HTTP :

  1. درخواست یه http.request از کلاینت به سرور ارسال میشه
  2. سرور یه کانکشن باز میکنه و اطلاعات لازمه رو توی scope اون سشن مینویسه
  3. پاسخ http.response را به کلاینت ارسال میکنه
  4. و کانکشن رو میبنده

اما توی وب سوکت فرق داره

  1. کلاینت یه درخواست میفرسته و اطلاعات scope رو سمت سرور ارسال میکنه
  2. سرور یه ایونت chat.recieved_message اجرا میکنه و میتونه جواب بده و اگر جواب بده ایونت chat.send_message رو اجرا کرده
  3. هر کدوم از اونها میتونن کانکشن رو ببندن

آیا Websocket ها Synchronous هستن یا Asynchronous؟

وب سوکت به همزمانی یا ناهمزمانی ربطی نداره و میتونه هردوش باشه. وب سوکت فقط یک پروتکل وب هست.

تغییر توی دیتابیس برای زدن query بهینه تر تو ORM جنگو

توی یه مدل جنگو میخاستم یه JSON ذخیره کنم، طبیعتا رفتم تو کار استفاده از JSONField. تعریف مدل اینجوری شد:

توی یه view میخاستم kwargs ای که به view پاس داده شده رو توی این فیلد ذخیره کنم بنابراین با ORM زیر اون رو آپدیت میکردم:

دیتایی که توی این فیلد ذخیره میشه حالات مختلفی داره مثل

نکته اینجاست که من میخام یه جاهای query بزنم و اون آبجکتهایی رو داشته باشم که این فلدشون پارامتر doc_id رو توش داره (همشون ندارن) و مجبور از روش زیر query بزنم:

مشکل این چیه؟ اینکه بهینه نیست باید راه حلی باشه که با یه query جواب رو بگیرم. در واقع یک ORM جنگو یکجا به SQL تبدیل بشه و یک هیت دیتابیس داشته باشه. فهمیدم که با تغییر زیر میتونم این کار رو بکنم

اول اینکه باید JSONField رو هست بگم که دیفالتش دیکشنری باشه

و حالا دیگه از json.dumps برای وارد کردن دیتا استفاده نمیکنیم:

و حالا دیتایی که توی دیتابیس ذخیره میشه این فیلدش دیکشنریه و میتونیم با یه query دیتایی که میخاستیم رو بگیریم:

چطور توی جنگو Django ترکیب چند فیلد رو یکتا کنیم و بعدش باید به چه نکاتی توجه کنیم

فرض کنید میخایم توی یک Model تو جنگو بگیم که مجموع سه تا فیلد باید یکتا باشه. برای این کار توی django.models.Model یک قابلیت تعریف شده:

ولی مساله اینه که اینجا ارور در سطح دیتابیس نشون میده و جاهای مختلف باید این Excelption رو هندل کنید. اینطوری نیست که خودش هندل کنه. مثلا

همینطور باید توی view این موضوع رو خودمون هندل کنیم و قبل از ارسال درخواست به دیتابیس حتما چک کنیم ترکیب این سه فیلد تکراری توی دیتابیس نباشه

و برای اینکه ارور رو توی template قشنگتر نشون بده مثل زیر عمل کنیم

استفاده بد از تکنولوژی

گاهی پیش میاد که توی کدهایی که میخونم از برنامه نویسهای دیگه مشکلات زیر رو توشون میبینم:

1.از یک Framework استفاده شده ولی از تمامی قابلیتهاش استفاده نشده و گاهی میبینیم قابلیتی که توی فریمورک به خوبی و استاندارد پیاده سازی شده رو استفاده نکرده و خود شخص اون قابلیت رو کد زده

2.کتابخانه یا فریمورکی نصب شده و قابلیتی داره ولی از اون استفاده نشده و برای اون قابلیت کتابخانه دیگری نصب شده

دلیل این اشتباه اینه که برنامه نویس مستندات اون فریمورک و کتابخانه رو نخونده. کد زیر مثالی از حالت اوله. ببینید در ادامه یک فرم جنگو Django میبینیم:

و کد زیر view هست که توی اون از این فرم استفاده شده:

مشکل کجاست؟ برنامه نویس اومده مقدار فیلدهایی که از template ارسال شده POST با استفاده از request.POST.get گرفته شده. بعد برنامه نویس اومده field validation (اعتبارسنجی فیلدها) رو انجام داده. بعد اگر توی ورودی فیلد ایرادی وجود داشت اومده یک پیام ارور به کاربر نشون داده. تمامی این کاری که برنامه نویس براش کد زده توی django.forms.Formپیاده سازی شده. به بهترین شکل توی جنگو پیاده سازی شده. در حالی که با تست این view مشکلات خیلی زیادی هست.

با استفاده از ServiceForm.is_valid() تمامی فیلدها validate و اعتبار سنجی میشن و اگر ایرادی داشته باشه همونجا فیلد قرمز رنگ میشه و ارور مرتبط نشان داده میشه اما در این حالت اگر یک فیلد required توی فرم خالی بمونه ارور مرتبط به کاربر نشان داده نمیشه. مشکل دوم اینه که مثلا اگر یک فیلد ایمیل یا پسورد باشه ایمیل با روش های استاندارد اعتبارسنجی میشه و پسورد هم همینطور.

مشکل بعدی این کد اینه که کد کثیفه و از طرفی همین الان ممکنه چنتا باگ داشته باشه که با ورودی های مختلف خودشو نشون میده و ما الان نمیدونیم وجود داره یا نه.

همینطور که توی این لینک میبینید فرم جنگو چند لایه صحت سنجی داره.

همیشه اگر از فریمورک یا کتابخانه ای استفاده میکنید حتما تمامی مستنداتش رو بخونید.

درگیری جدید با django-ckeditor-5

برای اضافه کردن فونت به CKEditor توی Django راه حل طبق این لینکه. منتها مساله اینه که من با نصب ورژن 5 نمیتونستم این مدلی فونت اضافه کنم.

توی django_ckeditor_5 تنظیمات فونت توی django_ckeditor_5/dist/bundle/js اضافه میشه. البته فقط تنظیمات فونت اینجوریه.

بخاطر همین فایل bundle.js رو prettify کردم و فونت ها رو سرچ کردم و Bnazanin رو بهش اضافه کردم مثل کد زیر

و یه فایل ساختم توی django_ckeditor_5/css/font.css و کد زیر رو زدم:

و البته دایرکتوری django_ckeditor_5/farsifonts/ رو ساختم و فایل ttf فونت رو کپی کردم توش.

و تمام

راه حل غیر استاندارده چون هر بار اگر بخام تغییر بدم باید bundle.js رو تغییر بدم درستش این بود که ورژن جدید CKEditor رو نصب کنم. ولی امکانش نبود.

تجربه جالب اختلال اتصال به ریپوزیتوی داخلی

پروژه ای رو توی یه سرور Redhat 8 دیپلوی کردیم و از اونجای که سرور لینک اینترنت نداشت و سرویس داخلی شبکه شرکت بود هیچ پکیجی رو نمیشد از repository های استاندارد Redhat نصب و آپدیت کرد.

پروژه رو داکرایز Dockerize کردیم و عملا یه شبکه و چند تا اینترفیس به Interface های لینوکس اضافه شد.

از شرکت یه repository لوکال گرفتیم چون چاره ای نبود. Nexus نصب کرده بودن و پکیج ها هم آپدیت بود. لینک هاشو گرفتم و ست کردیم روی ردهت ولی آپدیت نمیشد و destination unreachable میداد هر دو توی شبکه داخلی و آی پی سرور از رنج 10.x.y.z و رنج آی پی ریپو 172.17.x.y هست telnet نداره و traceroute ( که توی ردهت tracepath هست و traceroute رو حذف کردن) نشون میده که پکتها توی خود سرور drop میشه.

اول فکر کردم که route ای وجود نداره ولی بعد از troubleshooting یه نکته جالب دیدم.

Docker به صورت پیشفرض یه اینترفیس مجازی به اسم docker0 میسازه که آی پی پیشفرضش تو رنج 171.17.x.y هست و به صورت اتفاقی توی رنج Nexus بود بنابراین پکتهای درخواست از ریپو به داکر ارسال میشه و چنتا کانتینر container ما هم پکت رو طبیعتا drop میکردن.

کانتینرها رو پایین آوردم

docker0 رو حذف کردم

و آپدیت درست شد Yum update

استفاده از CKEditor توی پروژه (جنگو)

اگر توی جنگو نیاز دارید که به جای یک Text Area خشک و بی روح یک ادیتور کامل داشته باشید (مثلا برای نوشتن بلاگ که به بولت چونت و هایلایت و …. نیاز دارید) میتونید از django-ckeditor-5 استفاده کنید. مستندات خود کتابخونه کامل نحوه نصب و تنظیمات رو گفته و نیازی به توضیح دادن کدش نیست. من قسمت هایی که برای جالب بود رو میگم:

یه کتابخونه کامله که بعد از نصبش و migrate کردن دیتابیس میتونید از فیلدهای جدیدی که اضافه میکنه توی ORM استفاده کنید مثلا به جای استفاده از فیلدهای استاندارد جنگو میشه از CKEditor5Field استفاده کرد

لازم نیست توی template کاری بکنید چون خود فایلهای استاتیک رو اضافه میکنه و اتوماتیک رندر میکنه.

توی تعریف فرمها میشه از widget هایی که اضافه کرده استفاده کرد. ولی یه بخشی که برای من خیلی جالب بود اینه که من نیاز داشتم از اون فرمهای رندر شده گاهی یه درخواست AJAX به یک route بفرستم و متوجه شدم مثل حالت معمولی نمیشه دیتای textarea رو با JQuery بگیرم و ازش توی request استفاده کنم. گشتم و متوجه شدم که توی تمپلیت ادیتور قابل دسترسی هست و از API هاش میتونم استفاده کنم. مثل کد زیر از editors لیست فیلدهایی که ckeditor بودن رو داشته باشم و با تابع getData() دیتای رندر شده شو بگیرم یعنی دیتایی که فرمت HTML داره و باید توی دیتابیس ذخیره بشه.

مشکل کندی در اپلیکیشن

اخیرا یک پروژه با بک اند Django رو review کردیم که مشکل کندی داشت، کارفرما دسترسی به سرور رو نداد و قرار شد توی کد ببینیم مشکل از کجاست.

به خوبی از امکانات جنگو استفاده شده بود، از کتابخانه خارجی بی مورد استفاده نشده بود و از ماژولهای داخلی جنگو استفاده شده بود. ساختار پروژه استاندارد بود و تغییرات من در آوردی (به اسم خلاقیت) نداشت و توسعه دهنده کاملا طبق مستندات رسمی جلو رفته. Model ها روابط پیچیده نداشتند، به بهترین شکل از ORM پیشفرض جنگو استفاده شده بود، مشکلی توی query هایی که توی View ها زده بود دیده نمی شد (مشکل n+1 رو توی دسترسی ها به دیتابیس رو بررسی کردیم، Query تکراری نداشت ) و …

Docker رو تست کردیم کانشکن بین container ها کند نبود، منابع کم نبود.

در نهایت به سرور دیتابیس و production سری زدیم، کاری که از اول باید میکردیم. توی dockerfile خود اپلیکیشن دیدیم اون رو با runserver اجرا کرده که طبیعیه که سرعت خیلی پایین باشه چون این صرفا برای توسعه و تست هست نه production. با Gunicorn دیپلوی کردیم و مشکل کندی حل شد.

Gunicorn پردازش concurrent رو میده و میشه بهش بگی روی چند تا worker اجرا بشه. Scalable هست و یه سری قابلیت دیگه که برای محیط عملیاتی نیازه مثل مدیریت لاگ و ارورها و …

ریزکاریهای تست جنگو با TransactionTestCase و TestCase

در پست قبل دیدیم که برای نوشتن تست در جنگو میتونیم از کلاس های django.test.TestCase و django.test.TransactionTestCase استفاده کرد (البته که از خود unittest پایتون هم می توان استفاده کرد اما این کار در جنگو منطقی نیست چرا که کلاسهای تعریف شده برای تست در جنگو قابلیت های زیادی برای تست اضافه کرده اضافه کرده. مثلا اگر با unittest پایتون استاندارد بخاید تستی بنویسید که نیاز به دسترسی دیتابیس یا اجرا اپلیکیشن وب داشته باشد قطعا به مشکل میخورید بنابراین هیچ کس این کار رو نمیکنه). دیدیم که یک کلاس دیگه به اسم django.test.LiveTestCase هم وجود داره که میتونید توی اون از کلاینت Selenium استفاده کنید که تست های لایو توی مرورگر رو داشته باشید.

سوالی که پیش میاد اینه که فرق TestCase و TransactionTestCase چیه؟ توی خود کد جنگو نوشته:

میگه هردو مثلا همه و توی بیشتر شرایط باید TestCase رو ترجیح بدید چون سریع تره مگر در شرایطی خاص باید TransactionTestCase رو استفاده کنید مثلا اونجا که تستی که مینویسید تراکنش به دیتابیس داره و باید انجام بشه. میگه توی دیتابیسی که از تراکنش پشتیبانی نمیکنه عملا هر دو یکی هستن مثلا دیتابیس MySQL با موتور MyISAM.

توی مستندات جنگو گفته که TransactionTestCase از SimpleTestCase ارثبری کرده و چنتا قابلیت استفاده کرده. مثلا دیتابیس رو بعد از هر متد تست به حالت تعریف شده بر میگردونه که این توی تست مهمه مثلا 100 تا تابع تست داریم باید بعد از هر کدوم دیتابیس ریست بشه که روی تست های دیگه تاثیر نذاره. TransactionTestCase اطلاعات رو ممکنه commit و rollback کنه و بعد از هر تست هم حتما دیتابیس رو truncate میکنه اما TestCase برای سرعت بیشتر truncate نداره و از تراکنش ها استفاده میکنه یعنی rollback میکنه. این کار باعث میشه توی تستهای که نیاز به تراکنش هست به مشکل بخورید مثلا select_for_update ها.

در کل چیزی که من تجربه کردم اگر توی توابع تست یک کلاس تستکیس، update یا insert دیتابیس دارید از TransactionTestCase استفاده کنید. اگر فقط select دارید از TestCase استفاده کنید که سرعت بیشتری داره. اگر کلا با دیتابیس کاری ندارید از SimpleTestCase استفاده کنید.