Update: 2026-08-07 01:35:27

This commit is contained in:
Hamza-Ayed
2026-08-07 01:35:27 +03:00
parent 0cb1ab0ea3
commit 5b864e30df
11 changed files with 843 additions and 26 deletions
+37
View File
@@ -69,6 +69,32 @@ try {
error_log("[accept_ride] تعذّر فحص الإيقاف: " . $eBlock->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP A0b — حارس الحصرية في الإسناد بالدفعات
//
// ‏بدون هذا الفحص تبقى الحصرية شكلية: سائق تلقّى العرض سابقاً (أو صاغ
// ‏الطلب يدوياً) يستطيع القبول خارج دوره فيعود السباق كما كان.
//
// ‏وجود المفتاح هو ما يحدد الوضع: البثّ الحر لا يكتبه أصلاً، فالفحص
// ‏يمرّ بلا أثر — لا حاجة لقراءة DISPATCH_MODE هنا.
// ═══════════════════════════════════════════════════════════
try {
if (isset($redisLocation) && $redisLocation) {
$currentOffer = $redisLocation->get("ride:$rideId:current_offer");
if ($currentOffer !== false && $currentOffer !== null
&& (string) $currentOffer !== (string) $driverId) {
error_log("[accept_ride] DriverID=$driverId حاول قبول RideID=$rideId"
. " والعرض الحالي لـ $currentOffer — رُفض");
printFailure("Ride not available");
exit;
}
}
} catch (Throwable $eOffer) {
// ‏فشل الفحص لا يمنع القبول — القفل التفاؤلي في STEP A يبقى الضامن
// ‏ضد الازدواج، وأسوأ نتيجة هنا هي عودة سلوك البثّ الحر لهذه الرحلة.
error_log("[accept_ride] تعذّر فحص حصرية العرض: " . $eOffer->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP A — القفل على ride DB (المرجع الأساسي)
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
@@ -93,6 +119,17 @@ try {
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
// ‏عدّاد القبول — بسط معدّل القبول الذي يرتّب به محرك الإسناد.
// ‏يُعدّ هنا بعد نجاح القفل حصراً: العدّ قبله كان سيحسب للخاسرين في
// ‏السباق قبولاً لم يحدث.
try {
if (isset($redisLocation) && $redisLocation) {
$redisLocation->hIncrBy("driver:score:$driverId", 'accepts', 1);
}
} catch (Throwable $eScore) {
error_log("[accept_ride] تعذّر تحديث عدّاد القبول: " . $eScore->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP B — تزامن primary DB (بعد نجاح القفل)
// ═══════════════════════════════════════════════════════════
+96 -23
View File
@@ -16,22 +16,20 @@ try {
// =================================================================================
// 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast)
// =================================================================================
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
// ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي
// بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل
// الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php.
//
// أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
// رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص
// يصل للسيرفر لاحقاً كنص "null".
$marketPayload = array_merge([
/**
* ‏حمولة الرحلة كما يراها السائق قبل القبول.
*
* ‏مشتركة بين البثّ الحر والإسناد بالدفعات عمداً: أي حقل يظهر في أحدهما
* ‏ولا يظهر في الآخر يصل للسيرفر لاحقاً كنص "null".
*
* ⚠️ ‏لا تضع هنا أي بيانات راكب حسّاسة (هاتف/إيميل/FCM token) — هذه الحمولة
* ‏تصل سائقين لن يقودوا الرحلة. التفاصيل تُجلب بعد نجاح القبول.
*
* ‏أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
* ‏رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح.
*/
function buildMarketPayload($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
return array_merge([
'id' => (string)$rideId,
'start_lat' => $lat,
'start_lng' => $lng,
@@ -48,6 +46,52 @@ function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketDa
'passengerRate' => $payloadData[33],
'passengerId' => $payloadData[7],
], $extraMarketData);
}
/**
* ‏يوقظ العرض المتسلسل في السوكِت بعد أن بنى dispatchStartBatched الطابور.
* ‏السوكِت هو صاحب المؤقّت لأنه العملية الوحيدة طويلة العمر (Workerman
* ‏Timer) — كرون كل دقيقة لا يصلح لمهلة عشر ثوانٍ.
*/
function notifySocketStartBatchedDispatch($rideId) {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'start_batched_dispatch',
'ride_id' => $rideId,
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 2000);
if ($INTERNAL_KEY) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
}
curl_exec($ch);
curl_close($ch);
}
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
// ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي
// بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل
// الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php.
//
// أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
// رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص
// يصل للسيرفر لاحقاً كنص "null".
$marketPayload = buildMarketPayload($rideId, $lat, $lng, $payloadData, $extraMarketData);
$postData = [
'action' => 'market_new_ride',
@@ -428,15 +472,44 @@ try {
}
}
if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
} else {
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
// ═══════════════════════════════════════════════════════════
// الإسناد — بالدفعات أو بالبثّ الحر
//
// ‏batched: عرض حصري متسلسل على مرشحين مرتبين. لا بثّ للسوق، ولا
// ‏إشعار جماعي — وإلا لضاع معنى الحصرية.
// ‏broadcast (الافتراضي): السلوك القائم حرفياً بلا أي تغيير.
// ═══════════════════════════════════════════════════════════
require_once __DIR__ . '/../dispatch/dispatch_helper.php';
$batchedStarted = false;
if (dispatchIsBatched((string) ($payload[31] ?? ''))) {
$batchedStarted = dispatchStartBatched(
$redisLocation ?? null,
$insertedId,
$driversData ?: [],
buildMarketPayload($insertedId, $startLat, $startLng, $payload, $extraDispatchData)
);
if ($batchedStarted) {
notifySocketStartBatchedDispatch($insertedId);
error_log("[add_ride] RideID=$insertedId أُسند بالدفعات.");
} else {
error_log("[add_ride] تعذّر الإسناد بالدفعات RideID=$insertedId"
. " — السقوط للبثّ الحر.");
}
}
// Broadcast للـ marketplace دائماً
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
if (!$batchedStarted) {
if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
} else {
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
}
// Broadcast للـ marketplace دائماً
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
}
// رد النجاح للتطبيق
printSuccess($insertedId);
@@ -328,6 +328,17 @@ try {
'passenger_id' => $passengerId,
]);
// ‏ختم نهاية الرحلة — مقياس الخمول في ترتيب محرك الإسناد (وزن ١٥٪،
// ‏إنصافاً لمن انتظر أطول). يُكتب هنا لا عند القبول: السائق يبدأ
// ‏"الخمول" حين يفرغ فعلاً، لا حين ينشغل.
try {
if (isset($redisLocation) && $redisLocation) {
$redisLocation->hSet("driver:score:$driver_id", 'last_ride_end', time());
}
} catch (Throwable $eScore) {
error_log("[finish_ride_updates] تعذّر ختم نهاية الرحلة: " . $eScore->getMessage());
}
// 🔥 [Fix Split-Brain] تحديث القاعدة البعيدة الآن فقط، بعد أن أصبح الدفع
// والتحديث المحلي مؤكدَين نجاحهما — يبقي الحالتين متطابقتين دائماً.
// فشل هذا التحديث best-effort فقط (لا يُرجع الرحلة المحلية المُنجَزة فعلاً).