Feature flags لكل tenant بدون spaghetti
ثلاث سنين تشغيل flags. البنية اللي قبلت توسّع. والبنية اللي ما قبلت.
جاري تحميل المقال…
ثلاث سنين تشغيل flags. البنية اللي قبلت توسّع. والبنية اللي ما قبلت.
جاري تحميل المقال…
feature flag هو سطر تفرّع منطقيّ احتاجه شخص ما مرّة. حين يكون لديك أربعون منها عبر codebase يخدم مئة مستأجر، «احتاجه مرّة» قد تراكم إلى شيء لا أحد يفهمه بالكامل. هذا المقال عن البنية التي منعت ذلك التراكم لنا، والبنية التي لم تفعل.
ثلاثة استخدامات صادقة، بترتيب صحّتها:
أيّ شيء آخر، غالباً لا تحتاج flag. اختبار A/B مشكلته الخاصّة بأدواته الخاصّة. flags طويلة الأجل لا يجرؤ أحد على إزالتها هي المشكلة التي يدور حولها هذا المقال.
استقررنا على جدول واحد:
CREATE TABLE feature_flags (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
key VARCHAR(120) NOT NULL, -- 'bulk_export'
description TEXT NOT NULL, -- السبب
owner_team VARCHAR(60) NOT NULL, -- من يزيله
rollout_kind ENUM('global', 'plan', 'tenant', 'percent') NOT NULL,
rollout_value JSON NOT NULL, -- حمولة لكلّ نوع
sunset_date DATE NOT NULL, -- حكم الإعدام
created_at TIMESTAMP NOT NULL,
UNIQUE KEY (key)
);ثلاث تفاصيل تكسب قيمتها:
owner_team مطلوب#flag بدون مالك هو flag لا يُزيله أحد. النسخة الأولى من هذا الجدول
تركته قابلاً للـ null؛ انتهى بنا الأمر بثلاثين flag وبدون وضوح حول من
يستطيع إيقاف أيّ منها. جعله NOT NULL وزرع «platform» كافتراضيّ
أجبر المحادثة.
sunset_date مطلوب#كلّ flag، حتّى الدائمة، لديه تاريخ يجب إعادة النظر فيه عنده. «إعادة
النظر» يمكن أن تعني الإزالة، يمكن أن تعني التأكيد، يمكن أن تعني
التمديد. المهمّ أنّ الجدول ليس للكتابة فقط. نشغّل وظيفة مجدولة تُرسل
بريداً إلى المالكين قبل ١٤ يوماً من الـ sunset؛ إن مرّ flag بدون
مراجعة، يحصل على tag STALE ويظهر باللون الأحمر على لوحة المنصّة.
rollout_kind و rollout_value مُطبَّعان#كلّ flag هو أحد أربعة أشكال:
global — value: {"enabled": true}. boolean للجميع.plan — value: {"plans": ["pro", "enterprise"]}. بوّابة فئة.tenant — value: {"tenant_ids": [42, 113, 7]}. قائمة صريحة.percent — value: {"percent": 25}. hash مستقرّ حسب tenant id.بإجبار كلّ flag على أحد أربعة أشكال، حصلنا على شيئَين: دالّة تقييم واحدة بدلاً من أربعين، والقدرة على سرد «كلّ الـ flags المفعّلة حالياً للمستأجر ٤٢» باستعلام واحد.
namespace App\Features;
class Features {
public function isOn(string $key, ?Tenant $tenant = null): bool {
$flag = $this->cache->remember(
"flag:{$key}",
60,
fn () => FeatureFlag::where('key', $key)->first()
);
if (!$flag) {
return false; // flag نسيناه يكون مُغلقاً
}
$value = $flag->rollout_value;
$tenant ??= app(TenantContext::class)->current();
return match ($flag->rollout_kind) {
'global' => (bool) ($value['enabled'] ?? false),
'plan' => $tenant && in_array($tenant->plan, $value['plans'] ?? [], true),
'tenant' => $tenant && in_array($tenant->id, $value['tenant_ids'] ?? [], true),
'percent' => $tenant
&& (crc32("{$key}:{$tenant->id}") % 100) < ((int) ($value['percent'] ?? 0)),
};
}
}ثلاثة أمور للملاحظة:
crc32("{key}:{tenant_id}")
حتميّ لكلّ (flag، tenant). نفس المستأجر يحصل على نفس الإجابة كلّ
مرّة، حتّى بعد إعادة التشغيل. هكذا تقوم بطرح تدريجيّ بدون قلب
المستخدمين داخلاً وخارجاً.جرّبنا بنيتَين انكسرتا حول العشرين flag.
FEATURE_BULK_EXPORT=true، FEATURE_DARK_MODE=enterprise، إلخ. عمل
لفترة. فشل حين احتجنا بوّابة لكلّ مستأجر: env vars لا يستطيعون قول
«المستأجر ٤٢ نعم، المستأجر ١١٣ لا» بدون مصفوفة، والمصفوفة أصبحت غير
قابلة للقراءة. أيضاً، كلّ تغيير تطلّب نشراً.
عمود config_overrides JSON على جدول tenants. عمل عند خمسة flags.
عند خمسة عشر، كان العمود جداراً من
{"feature_x": true, "feature_y": false, ...} لكلّ مستأجر، بدون
مكان مركزيّ للسؤال «ماذا تعني feature_x» أو «من يملكها». البنية وضعت
البيانات على المستأجر، لكنّ الـ flags تنتمي إلى الميزات، لا إلى
المستأجرين.
كلّ صباح جمعة، بوت Slack ينشر قائمة الـ flags التي يقع تاريخ sunset
الخاصّ بها في الـ ٣٠ يوماً القادمة، مع مالكيها. المالك لديه أسبوع
إمّا: تأكيد وتمديد، إزالة الـ flag (احذف الصفّ في الجدول مع نداءات
Features::isOn)، أو تحويله إلى قدرة دائمة (أضِف إلى جدول الفئات).
في الشهر الأوّل الذي فعلنا فيه هذا، أزلنا تسعة flags. ثلاثة منها كانت مفعّلة لأكثر من سنة بدون اعتراضات. نصف هؤلاء كانوا ميزات «تجريبيّة» شُحنت منذ زمن طويل للجميع.
بعد ثمانية عشر شهراً على البنية الجديدة، كان لدينا ستّة وأربعون flag نشط. لوحة المعلومات تتسع لشاشة واحدة. flags جديدة تستغرق خمس دقائق للإضافة (إدخال + مرجع). flags قديمة تتقاعد حسب الجدول. لم يحتج أيّ مهندس للسؤال «ماذا تعني الميزة X» منذ أكثر من سنة.
الطقس أهمّ من schema الجدول. الـ schema مجرّد السطح الذي يجعل الطقس ممكناً.
الجزء التالي من سلسلة تعدّد المستأجرين عن صادرات بيانات المستأجرين بدون كسر قاعدة البيانات. صفحة السلسلة تتابع البقيّة.