
פְּתִיחַ
האתר שלי, שאני עושה כתחביב, נועד לאחסון דפי בית ואתרים אישיים מעניינים. נושא זה התחיל לעניין אותי ממש בתחילת דרכי בתכנות, באותה תקופה הוקסמתי ממציאת אנשי מקצוע מעולים שכותבים על עצמם, על התחביבים והפרויקטים שלהם. ההרגל לגלות אותם בעצמי נותר גם עכשיו: כמעט בכל אתר מסחרי ולא כל כך מסחרי אני ממשיך לחפש בתחתית העמוד בחיפוש אחר קישורים לכותבים.
יישום הרעיון
הגרסה הראשונה הייתה פשוט דף HTML באתר האישי שלי, שבו שמתי קישורים עם חתימות ברשימת ul. לאחר שאספתי כ-20 עמודים לאורך זמן, התחלתי לחשוב שזה לא יעיל במיוחד והחלטתי לנסות להפוך את התהליך לאוטומטי. ב-stackoverflow, שמתי לב שאנשים רבים מציינים אתרים בפרופילים שלהם, אז כתבתי מנתח ב-php שפשוט עבר על הפרופילים, החל מהראשון (כתובות ב-SO עד היום נראות כך: `/users/1`), חילצתי קישורים מהתג הנדרש ושמתי אותם ב-SQLite.
ניתן לכנות זאת הגרסה השנייה: אוסף של עשרת אלפים כתובות URL בטבלת SQLite, שהחליף את הרשימה הסטטית ב-HTML. ביצעתי חיפוש פשוט ברשימה זו. מכיוון שהיו רק כתובות URL, החיפוש היה פשוט עליהן.
בשלב זה נטשתי את הפרויקט וחזרתי אליו לאחר זמן רב. בשלב זה ניסיון העבודה שלי כבר היה יותר משלוש שנים והרגשתי שאני יכול לעשות משהו רציני יותר. בנוסף, היה רצון גדול לשלוט בטכנולוגיות שהיו חדשות יחסית עבורי.
גרסה מודרנית
לאחר פרוסות ב-docker, מסד הנתונים הועבר ל-mongoDb, ולאחרונה יחסית נוסף redis, ששימש בתחילה רק לאחסון במטמון. אחת ממיקרו-פריימוורקס של PHP משמשת כבסיס.
בעיה
אתרים חדשים מתווספים על ידי פקודת קונסולה שמבצעת באופן סינכרוני את הפעולות הבאות:
- מוריד תוכן לפי כתובת URL
- מגדיר דגל המציין האם HTTPS היה זמין
- שומר על מהות האתר
- קוד ה-HTML והכותרות המקוריות נשמרים בהיסטוריית ה"אינדקסציה".
- מנתח תוכן, מחלץ כותרת ותיאור
- שומר נתונים באוסף נפרד
זה היה מספיק כדי פשוט לאחסן אתרים ולהציג אותם ברשימה:

אבל הרעיון של אינדוקס, סיווג ודירוג אוטומטיים של הכל, תוך שמירה על עדכניות הכל, לא התאים היטב לפרדיגמה הזו. אפילו הוספת שיטת אינטרנט להוספת דפים דרשה שכפול קוד וחסימה כדי למנוע DDoS פוטנציאלי.
באופן כללי, כמובן, הכל יכול להיעשות באופן סינכרוני, ובשיטת web, פשוט לשמור את כתובת ה-URL כדי שהשד המפלצתי יוכל לבצע את כל המשימות עבור כתובות ה-URL מהרשימה. אבל אפילו כאן, המילה "תור" עולה בראש. ואם מיישמים תור, אז אפשר לחלק את כל המשימות ולבצע אותן לפחות באופן אסינכרוני.
החלטה
הטמע תורים וצור מערכת מונחית אירועים לעיבוד כל המשימות. ורציתי לנסות את Redis Streams כבר הרבה זמן.
שימוש בזרמי Redis ב-PHP
מכיוון שהפריימורק שלי אינו אחד משלושת הענקים Symfony, Laravel ו-Yii, הייתי רוצה למצוא ספרייה עצמאית. אבל, כפי שהתברר (במבט ראשון) - בלתי אפשרי למצוא ספריות רציניות נפרדות. כל מה שקשור לתורים הוא או פרויקט מ-3 קומיטים לפני חמש שנים, או קשור לפריימורק.
שמעתי על Symfony כספקית של כמה רכיבים שימושיים, ואני כבר משתמש בכמה מהם. וגם כמה דברים מ-Laravel ניתנים לשימוש, למשל ה-ORM שלהם, ללא נוכחות של ה-framework עצמו.
סימפוניה/מסנג'ר
המועמד הראשון נראה לי מיד אידיאלי והתקנתי אותו ללא ספק. אבל חיפוש בגוגל של דוגמאות לשימוש מחוץ ל-Symfony התגלה כקשה יותר. איך להרכיב אוטובוס הודעות מחבורה של מחלקות עם שמות אוניברסליים וחסרי משמעות, ועוד על Redis?

התיעוד באתר הרשמי היה די מפורט, אבל האתחול תואר רק עבור Symfony באמצעות YML האהובה שלהם ושיטות קסומות אחרות עבור מי שאינו סימפוניסט. לא היה לי עניין בתהליך ההתקנה עצמו, במיוחד במהלך חופשת ראש השנה. אבל הייתי צריך לעשות את זה, ובאופן בלתי צפוי במשך זמן רב.
לנסות להבין כיצד ליצור מופעים של המערכת באמצעות מקורות Symfony זו גם לא המשימה הכי טריוויאלית עבור דד-ליינים צפופים:

אחרי שחיטטתי בכל זה וניסיתי לעשות משהו עם הידיים, הגעתי למסקנה שאני משתמש בקביים כלשהם והחלטתי לנסות משהו אחר.
להאיר/להיות בתור
התברר שהספרייה הזו קשורה קשר הדוק לתשתית Laravel ולחבורה של תלויות אחרות, אז לא ביליתי בה הרבה זמן: התקנתי אותה, הסתכלתי עליה, ראיתי את התלויות ומחקתי אותה.
yiisoft/yii2-תור
ובכן, כאן זה מיד נאמר מהשם, שוב קישור נוקשה ל-Yii2. הייתי צריך להשתמש בספרייה הזו והיא לא הייתה רעה, אבל לא חשבתי שהיא תלויה לחלוטין ב-Yii2.
השאר
כל שאר הפרויקטים שמצאתי ב-GitHub היו לא אמינים, מיושנים ונטושים ללא כוכבים, מזלגות או מספר רב של קומיטים.
חזרה ל-symfony/messenger, פרטים טכניים
הייתי צריך להבין את הספרייה הזו ואחרי שהקדשתי עוד קצת זמן הצלחתי. התברר שהכל די תמציתי ופשוט. כדי ליצור מופע של האוטובוס יצרתי מפעל קטן, כי הנחתי שיהיו לי כמה אוטובוסים עם מנהלי התקן שונים.

רק כמה צעדים:
- צור מטפלי הודעות שאמורים להיות ניתנים לקריאה פשוטה
- אנחנו עוטפים אותם ב-HandlerDescriptor (מחלקה מהספרייה)
- אנו עוטפים את ה"תיאורים" הללו בתוך מופע של HandlersLocator
- הוסף את HandlersLocator למופע MessageBus
- אנו מעבירים קבוצה של `SenderInterface` ל-SendersLocator, במקרה שלי מופעים של מחלקות `RedisTransport`, אשר מוגדרות בצורה ברורה.
- הוסף את SendersLocator למופע MessageBus
ל-MessageBus יש מתודה `->dispatch()` שמחפשת מטפלים מתאימים ב-HandlersLocator ומעבירה אליהם את ההודעה באמצעות ה-`SenderInterface` המתאים כדי לשלוח אותה דרך האפיק (זרמי Redis).
בתצורת הקונטיינר (במקרה זה php-di) ניתן להגדיר את כל החבילה הזו כך:
CONTAINER_REDIS_TRANSPORT_SECRET => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_TRANSPORT_LOG => function (ContainerInterface $c) {
return new RedisTransport(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER))
;
},
CONTAINER_REDIS_STREAM_RECEIVER_SECRET => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_SECRET),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_RECEIVER_LOG => function (ContainerInterface $c) {
return new RedisReceiver(
$c->get(CONTAINER_REDIS_STREAM_CONNECTION_LOG),
$c->get(CONTAINER_SERIALIZER)
);
},
CONTAINER_REDIS_STREAM_BUS => function (ContainerInterface $c) {
$sendersLocator = new SendersLocator([
AppMessagesSecretJsonMessages::class => [CONTAINER_REDIS_TRANSPORT_SECRET],
AppMessagesDaemonLogMessage::class => [CONTAINER_REDIS_TRANSPORT_LOG],
], $c);
$middleware[] = new SendMessageMiddleware($sendersLocator);
return new MessageBus($middleware);
},
CONTAINER_REDIS_STREAM_CONNECTION_SECRET => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'secret',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
CONTAINER_REDIS_STREAM_CONNECTION_LOG => function (ContainerInterface $c) {
$host = 'bu-02-redis';
$port = 6379;
$dsn = "redis://$host:$port";
$options = [
'stream' => 'log',
'group' => 'default',
'consumer' => 'default',
];
return Connection::fromDsn($dsn, $options);
},
כאן ניתן לראות שב-SendersLocator הקצנו "הובלה" שונה עבור שתי הודעות שונות, שלכל אחת מהן יש חיבור משלה לזרמים המתאימים.
יצרתי פרויקט הדגמה נפרד המדגים יישום של שלושה דמונים שמתקשרים זה עם זה באמצעות אפיק כזה: .
אבל אני אראה כיצד ניתן לבנות צרכן:
use AppMessagesDaemonLogMessage;
use SymfonyComponentMessengerHandlerHandlerDescriptor;
use SymfonyComponentMessengerHandlerHandlersLocator;
use SymfonyComponentMessengerMessageBus;
use SymfonyComponentMessengerMiddlewareHandleMessageMiddleware;
use SymfonyComponentMessengerMiddlewareSendMessageMiddleware;
use SymfonyComponentMessengerTransportSenderSendersLocator;
require_once __DIR__ . '/../vendor/autoload.php';
/** @var PsrContainerContainerInterface $container */
$container = require_once('config/container.php');
$handlers = [
DaemonLogMessage::class => [
new HandlerDescriptor(
function (DaemonLogMessage $m) {
error_log('DaemonLogHandler: message handled: / ' . $m->getMessage());
},
['from_transport' => CONTAINER_REDIS_TRANSPORT_LOG]
)
],
];
$middleware = [];
$middleware[] = new HandleMessageMiddleware(new HandlersLocator($handlers));
$sendersLocator = new SendersLocator(['*' => [CONTAINER_REDIS_TRANSPORT_LOG]], $container);
$middleware[] = new SendMessageMiddleware($sendersLocator);
$bus = new MessageBus($middleware);
$receivers = [
CONTAINER_REDIS_TRANSPORT_LOG => $container->get(CONTAINER_REDIS_STREAM_RECEIVER_LOG),
];
$w = new SymfonyComponentMessengerWorker($receivers, $bus, $container->get(CONTAINER_EVENT_DISPATCHER));
$w->run();
שימוש במסגרת זו באפליקציה
לאחר שיישמתי את ה-bus ב-backend שלי, הפרדתי שלבים בודדים מהפקודה הסינכרונית הישנה ויצרתי מטפלים נפרדים, שכל אחד מהם עושה את העבודה שלו.
תהליך הוספת אתר חדש למסד הנתונים התברר ככזה:

ומיד לאחר מכן נהיה לי הרבה יותר קל להוסיף פונקציונליות חדשה, למשל, חילוץ וניתוח RSS. מכיוון שתהליך זה דורש גם את התוכן המקורי, כלי חילוץ הקישורים של RSS, כמו WebsiteIndexHistoryPersistor, נרשם להודעת "Content/HtmlContent", מעבד אותה ומעביר את ההודעה הנדרשת הלאה לאורך הצינור שלו.

בסופו של דבר, היו כמה דמונים, שכל אחד מהם מחזיק בחיבורים רק למשאבים הדרושים. לדוגמה, הדמון סורקים מכיל את כל המטפלים הדורשים גישה לאינטרנט עבור תוכן, ואת הדמון להתמיד שומר על קשר עם מסד הנתונים.
כעת, במקום לבחור מתוך מסד הנתונים, המזהים הנדרשים לאחר הוספתם על ידי ה-persister פשוט מועברים דרך האפיק לכל המטפלים המעוניינים.
מקור: www.habr.com
