لن يكتب معظم المهندسين MUMPS في الإنتاج أبداً. كتبتُها — في حزمتي الصيدلية والفوترة في VistA، تعمل على Linux-based fis GT.M، على نطاق يغطي الرعاية الصحية الوطنية. في كل مرة أقول هذا في مؤتمر يسألني أحد إن كنت أمزح. لست أمزح. هكذا بدا الأمر فعلاً.
VistA ليست تجريداً، إنها قاعدة كود
VistA (Veterans Health Information Systems and Technology Architecture) هو نظام EHR مفتوح المصدر للـVA. كان في تطوير مستمر منذ السبعينيات. حزمة الصيدلية فيه واحدة من أقدم وأكثر أنظمة صرف الأدوية اختباراً واستمراراً على هذا الكوكب. حين أقول “مختبر للمعركة” أعني: نجا من أكثر من 40 عاماً من مبرمجي الـVA، ودورات الميزانية، وإصدارات MUMPS، وهجرات الأجهزة، والكونغرس.
قاعدة الكود ليست جميلة. ليست مُصمَّمة. إنها مُتراكمة — طُبِّقت وزيدت ورُقِّعت وامتُدَّت من أشخاص معظمهم متوفون أو متقاعدون. الروتينات لها ترويسات تعليقات من 1987. ستجد `; MODIFIED BY DPT 3/12/89 — DO NOT DELETE` ولن تحذف بالتأكيد ما بعده، لأنك لا تعرف ما يفعله ولا يعرف ذلك أي شخص لا يزال موظفاً.
هكذا يبدو الإنتاج حين يكون له 40 عاماً من الـuptime.
GT.M هي MUMPS، لكنها تعمل على Linux وهي سريعة جداً
حين يقول الناس “MUMPS”، يعنون في الغالب أحد runtimes: Caché (الآن IRIS) من InterSystems، أو GT.M من FIS (سابقاً Greystone Technology، ومنه الـG). GT.M مفتوح المصدر، يعمل على Linux، وهو ما تستخدمه معظم تثبيتات VistA. إنه الـruntime الذي تستخدمه الـVA. إنه الـruntime الذي استخدمته.
GT.M — وأريد أن أكون حذراً هنا — مُبهر بصدق كمحرك تخزين. ينفّذ globals الـMUMPS كـB-tree على القرص مع أمان crash على مستوى الـjournal. عملية SET ^PSDRUG(drugIen,"QTY")=qty ليست رحلة ذهاب وإياب لخادم قاعدة بيانات. إنها كتابة إلى B-tree محلي ستُرحّل وتُجوْرَن بواسطة GT.M. العملية التي تنفّذ روتين MUMPS ومحرك التخزين هي نفس العملية. لا network hop. لا ORM. لا مُخطّط استعلام.
هذا يبدو كلعبة حتى تنظر إلى معايير إنتاجية الصرف وتتساءل لماذا نظام من التسعينيات على أجهزة متواضعة يتفوق على Spring Boot API اللامع الخاص بك في حمل العمل الثقيل على الكتابة. ثم يتضح: لا يوجد شيء بين منطق التطبيق والبتات.
كيف بدت كتابة روتينات الصيدلية فعلاً
حزمة الصيدلية تدير مخزون الأدوية، وأوامر الصرف، وفحوصات التفاعل الدوائي، وسجلات الخلطات الوريدية. في MUMPS. على globals الـ^PS* (PS = Pharmacy System).
روتين الصرف يبدو تقريباً كالتالي:
DISPENSE(DFN,DRUG,QTY) ;dispense DRUG to patient DFN
N RESULT,AVAIL
L +^PSDRUG(DRUG,"STOCK"):5 E D ERRLK Q
S AVAIL=$G(^PSDRUG(DRUG,"QTY"))
I AVAIL<QTY D INSUF Q
S ^PSDRUG(DRUG,"QTY")=AVAIL-QTY
S ^PSDRUG(DRUG,"LAST")=$$NOW^XLFDT()
S RESULT="OK"
L -^PSDRUG(DRUG,"STOCK")
Q RESULT
تفضّل، حدّق في ذلك. سأنتظر.
N هو NEW (نطاق متغير محلي). L +^GLOBAL:timeout هو اكتساب قفل. $G() هو GET مع قيمة افتراضية (لا أخطاء null pointer، MUMPS لديها $G). S هو SET. Q هو QUIT. $$ يستدعي دالة خارجية — $$NOW^XLFDT() يستدعي تسمية NOW في روتين XLFDT، التي تُرجع timestamp بتنسيق FileMan (تاريخ FileMan قصة كاملة أخرى، لا تبدأها).
الـglobal ^PSDRUG هرمي: المُفهرِس ذو المستوى الأعلى هو رقم الإدخال الداخلي للدواء (IEN)، وتحته مفاتيح فرعية مُسمّاة كـ”QTY” و”STOCK” و”LAST”. هذا هو الـschema. لا ملف schema. الـschema ضمني في الكود. تتعلمه بقراءة الكود والتحديق في globals باستخدام أداة D ^%G في GT.M.
فحص التفاعل الدوائي وحش خاص
أكثر الكود الذي كتبته في VistA إثارة للقلق كان متعلقاً بفحوصات التفاعل الدوائي. globals الـ^PSSDI و^PSDRUG تحمل بيانات التفاعل، وحزمة الصيدلية لديها روتينات تُطلَق قبل تأكيد أمر الصرف للتحقق من أن الدواء الجديد لا يتفاعل مع أي شيء في قائمة الأدوية النشطة للمريض.
ما يجعل هذا مُجهداً ليس MUMPS. MUMPS مجرد بناء جملة. ما يجعله مُجهداً هو أن المنطق هو شبكة الأمان. لا خدمة downstream تتحقق من عملك. يعمل الروتين، يرى الصيدلاني النتيجة، وإذا كان في منطق فحص التفاعل الخاص بك خطأ، يُصرَف دواء كان يجب أن يُحذَّر منه.
اختبرت تلك الروتينات بهوس. أصبح M-Unit في GT.M (إطار اختبار يبدو كـJUnit لو صُمِّم JUnit عام 1995) أفضل أصدقائي. أعددت سيناريوهات بتفاعلات معروفة — warfarin والأسبرين، methotrexate والـNSAIDs — وشغّلتها حتى كانت الأعلام متسقة. ربما كان هذا الوقت الأكثر حرصاً في حياتي المهنية على قطعة كود.
حزمة رسائل HL7 حيث أصبحت الأمور غريبة
حزمة HL7 في VistA تسبق في بعض الحالات تنظيف مواصفات HL7 v2 بسنوات. الروتينات التي تحلل وتولّد رسائل HL7 تعمل على تقطيع strings في MUMPS — $E(MSG,start,end) لاستخراج المقاطع، _ للتسلسل، دوال piece للتقسيم على المحدّدات.
ما وجدته هناك: حالات حدية في مقاطع ديموغرافيات المريض كانت تُعالَج بتعليقات `; KLUDGE — MO WILL FIX LATER` من أشخاص لم يعودوا واضحاً. أصلحت بعضها. تركت ملاحظات للباقي. هكذا يبقى الكود القديم — ليس بالتنظيف، بل بتراكم تعليقات متزايدة الإفادة حول الكودات الحاملة للحمل.
عمل GT.M DB connector في AFAQ
بعد وقتي في VistA في EHS، انتقلت إلى AFAQ حيث كنا نبني جناح EHR/EMR مبنياً جزئياً على مكونات VA VistA. الفجوة التي واجهتها فوراً: GT.M لم يكن له connector قاعدة بيانات حديث. لا JDBC. لا REST adapter. لا طريقة لتطبيق Java أو frontend ويب للتحدث إليه دون الدخول في MUMPS الخام أو استخدام طبقة RPC قديمة قائمة على TCP تعود للتسعينيات.
فبنيت connectors جديدة. التحدي هو أن آلية الوصول الأصلية لـGT.M هي إما MUMPS داخل العملية أو C API لـ$gtm_dist — مكتبة C تتيح استدعاء روتينات GT.M والوصول للـglobals من C. غلّفنا ذلك في طبقة JNI حتى يستطيع الـbackend لـSpring Boot استدعاءها مباشرة. لم تكن جميلة. معالجة الأخطاء عبر حدود JNI كانت تجربة إبداعية بشكل خاص. لكنها نجحت، وقطعت latency رحلة الذهاب والإياب بما يكفي لانتقال وقت تحميل مخطط الـEHR من “محرج” إلى “مقبول.”
الفهم الأعمق من ذلك المشروع: GT.M سريع لأنه بسيط. كل طبقة تجريد تُضيفها فوقه — REST وJNI وTCP RPC — تكلفك شيئاً من تلك البساطة. الحيلة إضافة بالضبط ما يكفي من التجريد ليستطيع النظام المستهلك التحدث إليه، ولا أكثر.
ما أخذته معي
كتابة MUMPS في الإنتاج في نظام رعاية صحية وطني النطاق ليست شيئاً أنصح به كمسار مهني. كما لن أتخلى عنه.
إليك ما علّمني إياه مما لا يُعلّمه عمل الـbackend الحديث:
- تكاليف التخزين موجودة دائماً؛ معظم اللغات تُخفيها فقط. في MUMPS، كل
S ^global(key)=valueهي عملية تخزين. لن تنسى أن الكتابات تكلّف شيئاً. في Java، تنسى ذلك باستمرار حتى تبدأ جلسة Hibernate في إجراء 400 استعلام لكل طلب. - تصميم الـschema لا يزال تصميماً حتى حين لا يوجد ملف schema. تسلسل مُفهرِسات الـglobal في MUMPS هو نموذج بياناتك. اختيارات مفاتيح سيئة تتتالى في أنماط وصول سيئة لا تستطيع إصلاحها دون إعادة كتابة كل ما يقرأ تلك globals.
- أمان الـcrash أهم مما تظن. journaling في GT.M يعني أن تثبيتات VistA تعمل لسنوات دون أحداث تلف بيانات. قواعد بيانات Postgres الخاصة بي تتطلب نظافة معاملات دقيقة لتحقيق نفس الضمان. نموذج MUMPS يجعل المسار الآمن هو المسار الافتراضي.
أكتب الآن معظم الوقت Java وTypeScript. قواعد بياناتي Postgres. رuntimes عندي لها GCs. لكن حين أراجع تصميم schema أو أتجادل حول حدود المعاملات، جزء من دماغي لا يزال في غلاف GT.M ذاك، يحدّق في globals الـ^PSDRUG ويفكر في كيف ستبدو القراءات الساعة 3 صباحاً خلال موجة الصرف.
هذه ليست ذكريات حنينية. إنها تعليم.