[{"data":1,"prerenderedAt":2241},["ShallowReactive",2],{"content:\u002F09-redis\u002F07-streams-pubsub-production":3},{"title":4,"description":5,"path":6,"body":7},"Redis Pub\u002FSub, Streams и production checklist","Redis умеет не только хранить ключи, но и передавать сообщения. Для этого есть два разных инструмента: Pub\u002FSub и Streams. Они похожи только внешне: оба позволяют отправлять события. По гарантиям, хранению и поведению при сбоях это разные модели.","\u002F09-redis\u002F07-streams-pubsub-production",{"type":8,"value":9,"toc":2217},"minimark",[10,14,17,28,31,34,39,42,48,51,73,76,90,93,96,98,102,107,110,116,119,125,128,135,137,141,144,150,153,159,162,164,168,179,185,196,202,205,207,211,214,1303,1310,1313,1333,1335,1339,1343,1346,1349,1366,1369,1375,1378,1384,1387,1389,1393,1396,1402,1405,1424,1427,1433,1436,1438,1442,1445,1451,1457,1460,1463,1465,1469,1473,1476,1505,1507,1511,1514,1646,1649,1669,1671,1675,1678,1714,1717,1720,1722,1726,1729,1751,1756,1758,1762,1768,1770,1774,1809,1811,1815,1843,1845,1849,1879,2029,2213],[11,12,4],"h1",{"id":13},"redis-pubsub-streams-и-production-checklist",[15,16,5],"p",{},[18,19,25],"pre",{"className":20,"code":22,"language":23,"meta":24},[21],"language-text","Pub\u002FSub:  сообщение летит подписчикам прямо сейчас\nStreams: сообщение записывается в log, потом читается consumer'ами\n","text","",[26,27,22],"code",{"__ignoreMap":24},[15,29,30],{},"Если выбрать не тот инструмент, можно получить потерю сообщений, бесконечные повторы или очередь, которую невозможно обслуживать.",[32,33],"hr",{},[35,36,38],"h2",{"id":37},"pubsub","Pub\u002FSub",[15,40,41],{},"Pub\u002FSub - fire-and-forget рассылка подписчикам.",[18,43,46],{"className":44,"code":45,"language":23,"meta":24},[21],"PUBLISH notifications \"course updated\"\nSUBSCRIBE notifications\n",[26,47,45],{"__ignoreMap":24},[15,49,50],{},"Свойства:",[52,53,54,58,61,64,67,70],"ul",{},[55,56,57],"li",{},"сообщение не сохраняется;",[55,59,60],{},"если subscriber offline, он не получит старые сообщения;",[55,62,63],{},"нет ack;",[55,65,66],{},"нет retry;",[55,68,69],{},"нет consumer groups;",[55,71,72],{},"хорошо подходит для realtime-сигналов.",[15,74,75],{},"Типичные сценарии:",[52,77,78,81,84,87],{},[55,79,80],{},"invalidation signal между инстансами приложения;",[55,82,83],{},"realtime notification fanout, если потеря допустима;",[55,85,86],{},"broadcast \"конфиг обновился\";",[55,88,89],{},"lightweight coordination.",[15,91,92],{},"Pub\u002FSub не подходит для задач, где событие должно быть обработано гарантированно.",[15,94,95],{},"Ещё одна тонкость: Pub\u002FSub не даёт backpressure для медленного subscriber'а на уровне бизнес-логики. Если получатель не успевает, он не может \"догнать\" историю после reconnect. Поэтому invalidation через Pub\u002FSub обычно совмещают с TTL или version check: потерянный сигнал не должен навсегда оставить stale cache.",[32,97],{},[35,99,101],{"id":100},"streams-и-consumer-groups","Streams и consumer groups",[103,104,106],"h3",{"id":105},"streams","Streams",[15,108,109],{},"Stream хранит события в append-only log.",[18,111,114],{"className":112,"code":113,"language":23,"meta":24},[21],"XADD lesson-events * type completed user_id 42 lesson redis-overview\nXRANGE lesson-events - +\n",[26,115,113],{"__ignoreMap":24},[15,117,118],{},"Каждое событие получает ID:",[18,120,123],{"className":121,"code":122,"language":23,"meta":24},[21],"1714471200000-0\n",[26,124,122],{"__ignoreMap":24},[15,126,127],{},"Первая часть примерно связана со временем, вторая - sequence внутри миллисекунды.",[15,129,130,131,134],{},"Stream можно читать напрямую через ",[26,132,133],{},"XREAD",", но в backend чаще интересны consumer groups.",[32,136],{},[103,138,140],{"id":139},"consumer-groups","Consumer groups",[15,142,143],{},"Consumer group позволяет нескольким worker'ам совместно читать один stream. Каждое сообщение внутри группы выдаётся одному consumer'у.",[18,145,148],{"className":146,"code":147,"language":23,"meta":24},[21],"XGROUP CREATE lesson-events progress-workers $ MKSTREAM\nXREADGROUP GROUP progress-workers worker-1 COUNT 10 BLOCK 5000 STREAMS lesson-events >\nXACK lesson-events progress-workers 1714471200000-0\n",[26,149,147],{"__ignoreMap":24},[15,151,152],{},"Схема:",[18,154,157],{"className":155,"code":156,"language":23,"meta":24},[21],"Stream: lesson-events\n  ├─ group: progress-workers\n  │    ├─ consumer: worker-1\n  │    └─ consumer: worker-2\n  └─ group: analytics-workers\n       └─ consumer: worker-3\n",[26,158,156],{"__ignoreMap":24},[15,160,161],{},"Разные группы читают независимо. Это похоже на то, как разные consumer groups в Kafka читают topic, но Redis Streams проще и обычно локальнее по масштабу.",[32,163],{},[103,165,167],{"id":166},"pending-entries","Pending entries",[15,169,170,171,174,175,178],{},"Когда consumer получил сообщение через ",[26,172,173],{},"XREADGROUP",", но ещё не сделал ",[26,176,177],{},"XACK",", сообщение попадает в Pending Entries List.",[18,180,183],{"className":181,"code":182,"language":23,"meta":24},[21],"delivered -> processing -> XACK -> done\n                 │\n                 └─ worker died -> pending remains\n",[26,184,182],{"__ignoreMap":24},[15,186,187,188,191,192,195],{},"Если worker умер, другой worker может забрать старые pending messages через ",[26,189,190],{},"XAUTOCLAIM"," или ",[26,193,194],{},"XCLAIM",".",[18,197,200],{"className":198,"code":199,"language":23,"meta":24},[21],"XAUTOCLAIM lesson-events progress-workers worker-2 60000 0-0 COUNT 100\n",[26,201,199],{"__ignoreMap":24},[15,203,204],{},"Это база для retry-механизма. Но Redis сам не знает, сколько раз вы пытались обработать сообщение и когда его отправить в DLQ. Эту логику проектирует приложение.",[32,206],{},[103,208,210],{"id":209},"go-worker-для-streams","Go worker для Streams",[15,212,213],{},"Пример worker'а с graceful shutdown:",[18,215,220],{"className":216,"code":217,"language":218,"meta":219,"style":24},"language-go shiki shiki-themes github-dark","package main\n\nimport (\n    \"context\"\n    \"errors\"\n    \"log\"\n    \"os\u002Fsignal\"\n    \"syscall\"\n    \"time\"\n\n    \"github.com\u002Fredis\u002Fgo-redis\u002Fv9\"\n)\n\nfunc main() {\n    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)\n    defer stop()\n\n    rdb := redis.NewClient(&redis.Options{Addr: \"localhost:6379\"})\n    defer func() {\n        if err := rdb.Close(); err != nil {\n            log.Printf(\"close redis: %v\", err)\n        }\n    }()\n\n    if err := ensureGroup(ctx, rdb, \"lesson-events\", \"progress-workers\"); err != nil {\n        log.Fatalf(\"ensure group: %v\", err)\n    }\n\n    worker := \"worker-1\"\n    for ctx.Err() == nil {\n        streams, err := rdb.XReadGroup(ctx, &redis.XReadGroupArgs{\n            Group:    \"progress-workers\",\n            Consumer: worker,\n            Streams:  []string{\"lesson-events\", \">\"},\n            Count:    10,\n            Block:    5 * time.Second,\n        }).Result()\n        if errors.Is(err, redis.Nil) {\n            continue\n        }\n        if err != nil {\n            if ctx.Err() != nil {\n                break\n            }\n            log.Printf(\"read group: %v\", err)\n            time.Sleep(time.Second)\n            continue\n        }\n\n        for _, stream := range streams {\n            for _, msg := range stream.Messages {\n                if err := handleMessage(ctx, msg); err != nil {\n                    log.Printf(\"handle message %s: %v\", msg.ID, err)\n                    continue\n                }\n                if err := rdb.XAck(ctx, \"lesson-events\", \"progress-workers\", msg.ID).Err(); err != nil {\n                    log.Printf(\"ack message %s: %v\", msg.ID, err)\n                }\n            }\n        }\n    }\n}\n\nfunc ensureGroup(ctx context.Context, rdb *redis.Client, stream, group string) error {\n    err := rdb.XGroupCreateMkStream(ctx, stream, group, \"$\").Err()\n    if err != nil && !errors.Is(err, redis.Nil) {\n        if err.Error() == \"BUSYGROUP Consumer Group name already exists\" {\n            return nil\n        }\n        return err\n    }\n    return nil\n}\n\nfunc handleMessage(ctx context.Context, msg redis.XMessage) error {\n    select {\n    case \u003C-ctx.Done():\n        return ctx.Err()\n    default:\n    }\n    log.Printf(\"message %s values=%v\", msg.ID, msg.Values)\n    return nil\n}\n","go","no-run",[26,221,222,235,242,252,265,275,285,295,305,315,320,330,336,341,353,377,389,394,431,441,471,494,500,506,511,545,565,571,576,587,609,637,648,654,676,687,702,713,727,733,738,751,769,775,781,799,811,816,821,826,843,859,881,907,913,919,954,976,981,986,991,996,1002,1007,1063,1089,1113,1133,1142,1147,1156,1161,1169,1174,1179,1215,1223,1241,1252,1261,1266,1291,1298],{"__ignoreMap":24},[223,224,227,231],"span",{"class":225,"line":226},"line",1,[223,228,230],{"class":229},"snl16","package",[223,232,234],{"class":233},"svObZ"," main\n",[223,236,238],{"class":225,"line":237},2,[223,239,241],{"emptyLinePlaceholder":240},true,"\n",[223,243,245,248],{"class":225,"line":244},3,[223,246,247],{"class":229},"import",[223,249,251],{"class":250},"s95oV"," (\n",[223,253,255,259,262],{"class":225,"line":254},4,[223,256,258],{"class":257},"sU2Wk","    \"",[223,260,261],{"class":233},"context",[223,263,264],{"class":257},"\"\n",[223,266,268,270,273],{"class":225,"line":267},5,[223,269,258],{"class":257},[223,271,272],{"class":233},"errors",[223,274,264],{"class":257},[223,276,278,280,283],{"class":225,"line":277},6,[223,279,258],{"class":257},[223,281,282],{"class":233},"log",[223,284,264],{"class":257},[223,286,288,290,293],{"class":225,"line":287},7,[223,289,258],{"class":257},[223,291,292],{"class":233},"os\u002Fsignal",[223,294,264],{"class":257},[223,296,298,300,303],{"class":225,"line":297},8,[223,299,258],{"class":257},[223,301,302],{"class":233},"syscall",[223,304,264],{"class":257},[223,306,308,310,313],{"class":225,"line":307},9,[223,309,258],{"class":257},[223,311,312],{"class":233},"time",[223,314,264],{"class":257},[223,316,318],{"class":225,"line":317},10,[223,319,241],{"emptyLinePlaceholder":240},[223,321,323,325,328],{"class":225,"line":322},11,[223,324,258],{"class":257},[223,326,327],{"class":233},"github.com\u002Fredis\u002Fgo-redis\u002Fv9",[223,329,264],{"class":257},[223,331,333],{"class":225,"line":332},12,[223,334,335],{"class":250},")\n",[223,337,339],{"class":225,"line":338},13,[223,340,241],{"emptyLinePlaceholder":240},[223,342,344,347,350],{"class":225,"line":343},14,[223,345,346],{"class":229},"func",[223,348,349],{"class":233}," main",[223,351,352],{"class":250},"() {\n",[223,354,356,359,362,365,368,371,374],{"class":225,"line":355},15,[223,357,358],{"class":250},"    ctx, stop ",[223,360,361],{"class":229},":=",[223,363,364],{"class":250}," signal.",[223,366,367],{"class":233},"NotifyContext",[223,369,370],{"class":250},"(context.",[223,372,373],{"class":233},"Background",[223,375,376],{"class":250},"(), syscall.SIGINT, syscall.SIGTERM)\n",[223,378,380,383,386],{"class":225,"line":379},16,[223,381,382],{"class":229},"    defer",[223,384,385],{"class":233}," stop",[223,387,388],{"class":250},"()\n",[223,390,392],{"class":225,"line":391},17,[223,393,241],{"emptyLinePlaceholder":240},[223,395,397,400,402,405,408,411,414,417,419,422,425,428],{"class":225,"line":396},18,[223,398,399],{"class":250},"    rdb ",[223,401,361],{"class":229},[223,403,404],{"class":250}," redis.",[223,406,407],{"class":233},"NewClient",[223,409,410],{"class":250},"(",[223,412,413],{"class":229},"&",[223,415,416],{"class":233},"redis",[223,418,195],{"class":250},[223,420,421],{"class":233},"Options",[223,423,424],{"class":250},"{Addr: ",[223,426,427],{"class":257},"\"localhost:6379\"",[223,429,430],{"class":250},"})\n",[223,432,434,436,439],{"class":225,"line":433},19,[223,435,382],{"class":229},[223,437,438],{"class":229}," func",[223,440,352],{"class":250},[223,442,444,447,450,452,455,458,461,464,468],{"class":225,"line":443},20,[223,445,446],{"class":229},"        if",[223,448,449],{"class":250}," err ",[223,451,361],{"class":229},[223,453,454],{"class":250}," rdb.",[223,456,457],{"class":233},"Close",[223,459,460],{"class":250},"(); err ",[223,462,463],{"class":229},"!=",[223,465,467],{"class":466},"sDLfK"," nil",[223,469,470],{"class":250}," {\n",[223,472,474,477,480,482,485,488,491],{"class":225,"line":473},21,[223,475,476],{"class":250},"            log.",[223,478,479],{"class":233},"Printf",[223,481,410],{"class":250},[223,483,484],{"class":257},"\"close redis: ",[223,486,487],{"class":466},"%v",[223,489,490],{"class":257},"\"",[223,492,493],{"class":250},", err)\n",[223,495,497],{"class":225,"line":496},22,[223,498,499],{"class":250},"        }\n",[223,501,503],{"class":225,"line":502},23,[223,504,505],{"class":250},"    }()\n",[223,507,509],{"class":225,"line":508},24,[223,510,241],{"emptyLinePlaceholder":240},[223,512,514,517,519,521,524,527,530,533,536,539,541,543],{"class":225,"line":513},25,[223,515,516],{"class":229},"    if",[223,518,449],{"class":250},[223,520,361],{"class":229},[223,522,523],{"class":233}," ensureGroup",[223,525,526],{"class":250},"(ctx, rdb, ",[223,528,529],{"class":257},"\"lesson-events\"",[223,531,532],{"class":250},", ",[223,534,535],{"class":257},"\"progress-workers\"",[223,537,538],{"class":250},"); err ",[223,540,463],{"class":229},[223,542,467],{"class":466},[223,544,470],{"class":250},[223,546,548,551,554,556,559,561,563],{"class":225,"line":547},26,[223,549,550],{"class":250},"        log.",[223,552,553],{"class":233},"Fatalf",[223,555,410],{"class":250},[223,557,558],{"class":257},"\"ensure group: ",[223,560,487],{"class":466},[223,562,490],{"class":257},[223,564,493],{"class":250},[223,566,568],{"class":225,"line":567},27,[223,569,570],{"class":250},"    }\n",[223,572,574],{"class":225,"line":573},28,[223,575,241],{"emptyLinePlaceholder":240},[223,577,579,582,584],{"class":225,"line":578},29,[223,580,581],{"class":250},"    worker ",[223,583,361],{"class":229},[223,585,586],{"class":257}," \"worker-1\"\n",[223,588,590,593,596,599,602,605,607],{"class":225,"line":589},30,[223,591,592],{"class":229},"    for",[223,594,595],{"class":250}," ctx.",[223,597,598],{"class":233},"Err",[223,600,601],{"class":250},"() ",[223,603,604],{"class":229},"==",[223,606,467],{"class":466},[223,608,470],{"class":250},[223,610,612,615,617,619,622,625,627,629,631,634],{"class":225,"line":611},31,[223,613,614],{"class":250},"        streams, err ",[223,616,361],{"class":229},[223,618,454],{"class":250},[223,620,621],{"class":233},"XReadGroup",[223,623,624],{"class":250},"(ctx, ",[223,626,413],{"class":229},[223,628,416],{"class":233},[223,630,195],{"class":250},[223,632,633],{"class":233},"XReadGroupArgs",[223,635,636],{"class":250},"{\n",[223,638,640,643,645],{"class":225,"line":639},32,[223,641,642],{"class":250},"            Group:    ",[223,644,535],{"class":257},[223,646,647],{"class":250},",\n",[223,649,651],{"class":225,"line":650},33,[223,652,653],{"class":250},"            Consumer: worker,\n",[223,655,657,660,663,666,668,670,673],{"class":225,"line":656},34,[223,658,659],{"class":250},"            Streams:  []",[223,661,662],{"class":229},"string",[223,664,665],{"class":250},"{",[223,667,529],{"class":257},[223,669,532],{"class":250},[223,671,672],{"class":257},"\">\"",[223,674,675],{"class":250},"},\n",[223,677,679,682,685],{"class":225,"line":678},35,[223,680,681],{"class":250},"            Count:    ",[223,683,684],{"class":466},"10",[223,686,647],{"class":250},[223,688,690,693,696,699],{"class":225,"line":689},36,[223,691,692],{"class":250},"            Block:    ",[223,694,695],{"class":466},"5",[223,697,698],{"class":229}," *",[223,700,701],{"class":250}," time.Second,\n",[223,703,705,708,711],{"class":225,"line":704},37,[223,706,707],{"class":250},"        }).",[223,709,710],{"class":233},"Result",[223,712,388],{"class":250},[223,714,716,718,721,724],{"class":225,"line":715},38,[223,717,446],{"class":229},[223,719,720],{"class":250}," errors.",[223,722,723],{"class":233},"Is",[223,725,726],{"class":250},"(err, redis.Nil) {\n",[223,728,730],{"class":225,"line":729},39,[223,731,732],{"class":229},"            continue\n",[223,734,736],{"class":225,"line":735},40,[223,737,499],{"class":250},[223,739,741,743,745,747,749],{"class":225,"line":740},41,[223,742,446],{"class":229},[223,744,449],{"class":250},[223,746,463],{"class":229},[223,748,467],{"class":466},[223,750,470],{"class":250},[223,752,754,757,759,761,763,765,767],{"class":225,"line":753},42,[223,755,756],{"class":229},"            if",[223,758,595],{"class":250},[223,760,598],{"class":233},[223,762,601],{"class":250},[223,764,463],{"class":229},[223,766,467],{"class":466},[223,768,470],{"class":250},[223,770,772],{"class":225,"line":771},43,[223,773,774],{"class":229},"                break\n",[223,776,778],{"class":225,"line":777},44,[223,779,780],{"class":250},"            }\n",[223,782,784,786,788,790,793,795,797],{"class":225,"line":783},45,[223,785,476],{"class":250},[223,787,479],{"class":233},[223,789,410],{"class":250},[223,791,792],{"class":257},"\"read group: ",[223,794,487],{"class":466},[223,796,490],{"class":257},[223,798,493],{"class":250},[223,800,802,805,808],{"class":225,"line":801},46,[223,803,804],{"class":250},"            time.",[223,806,807],{"class":233},"Sleep",[223,809,810],{"class":250},"(time.Second)\n",[223,812,814],{"class":225,"line":813},47,[223,815,732],{"class":229},[223,817,819],{"class":225,"line":818},48,[223,820,499],{"class":250},[223,822,824],{"class":225,"line":823},49,[223,825,241],{"emptyLinePlaceholder":240},[223,827,829,832,835,837,840],{"class":225,"line":828},50,[223,830,831],{"class":229},"        for",[223,833,834],{"class":250}," _, stream ",[223,836,361],{"class":229},[223,838,839],{"class":229}," range",[223,841,842],{"class":250}," streams {\n",[223,844,846,849,852,854,856],{"class":225,"line":845},51,[223,847,848],{"class":229},"            for",[223,850,851],{"class":250}," _, msg ",[223,853,361],{"class":229},[223,855,839],{"class":229},[223,857,858],{"class":250}," stream.Messages {\n",[223,860,862,865,867,869,872,875,877,879],{"class":225,"line":861},52,[223,863,864],{"class":229},"                if",[223,866,449],{"class":250},[223,868,361],{"class":229},[223,870,871],{"class":233}," handleMessage",[223,873,874],{"class":250},"(ctx, msg); err ",[223,876,463],{"class":229},[223,878,467],{"class":466},[223,880,470],{"class":250},[223,882,884,887,889,891,894,897,900,902,904],{"class":225,"line":883},53,[223,885,886],{"class":250},"                    log.",[223,888,479],{"class":233},[223,890,410],{"class":250},[223,892,893],{"class":257},"\"handle message ",[223,895,896],{"class":466},"%s",[223,898,899],{"class":257},": ",[223,901,487],{"class":466},[223,903,490],{"class":257},[223,905,906],{"class":250},", msg.ID, err)\n",[223,908,910],{"class":225,"line":909},54,[223,911,912],{"class":229},"                    continue\n",[223,914,916],{"class":225,"line":915},55,[223,917,918],{"class":250},"                }\n",[223,920,922,924,926,928,930,933,935,937,939,941,944,946,948,950,952],{"class":225,"line":921},56,[223,923,864],{"class":229},[223,925,449],{"class":250},[223,927,361],{"class":229},[223,929,454],{"class":250},[223,931,932],{"class":233},"XAck",[223,934,624],{"class":250},[223,936,529],{"class":257},[223,938,532],{"class":250},[223,940,535],{"class":257},[223,942,943],{"class":250},", msg.ID).",[223,945,598],{"class":233},[223,947,460],{"class":250},[223,949,463],{"class":229},[223,951,467],{"class":466},[223,953,470],{"class":250},[223,955,957,959,961,963,966,968,970,972,974],{"class":225,"line":956},57,[223,958,886],{"class":250},[223,960,479],{"class":233},[223,962,410],{"class":250},[223,964,965],{"class":257},"\"ack message ",[223,967,896],{"class":466},[223,969,899],{"class":257},[223,971,487],{"class":466},[223,973,490],{"class":257},[223,975,906],{"class":250},[223,977,979],{"class":225,"line":978},58,[223,980,918],{"class":250},[223,982,984],{"class":225,"line":983},59,[223,985,780],{"class":250},[223,987,989],{"class":225,"line":988},60,[223,990,499],{"class":250},[223,992,994],{"class":225,"line":993},61,[223,995,570],{"class":250},[223,997,999],{"class":225,"line":998},62,[223,1000,1001],{"class":250},"}\n",[223,1003,1005],{"class":225,"line":1004},63,[223,1006,241],{"emptyLinePlaceholder":240},[223,1008,1010,1012,1014,1016,1020,1023,1025,1028,1030,1033,1035,1037,1039,1042,1044,1047,1049,1052,1055,1058,1061],{"class":225,"line":1009},64,[223,1011,346],{"class":229},[223,1013,523],{"class":233},[223,1015,410],{"class":250},[223,1017,1019],{"class":1018},"s9osk","ctx",[223,1021,1022],{"class":233}," context",[223,1024,195],{"class":250},[223,1026,1027],{"class":233},"Context",[223,1029,532],{"class":250},[223,1031,1032],{"class":1018},"rdb",[223,1034,698],{"class":229},[223,1036,416],{"class":233},[223,1038,195],{"class":250},[223,1040,1041],{"class":233},"Client",[223,1043,532],{"class":250},[223,1045,1046],{"class":1018},"stream",[223,1048,532],{"class":250},[223,1050,1051],{"class":1018},"group",[223,1053,1054],{"class":229}," string",[223,1056,1057],{"class":250},") ",[223,1059,1060],{"class":229},"error",[223,1062,470],{"class":250},[223,1064,1066,1069,1071,1073,1076,1079,1082,1085,1087],{"class":225,"line":1065},65,[223,1067,1068],{"class":250},"    err ",[223,1070,361],{"class":229},[223,1072,454],{"class":250},[223,1074,1075],{"class":233},"XGroupCreateMkStream",[223,1077,1078],{"class":250},"(ctx, stream, group, ",[223,1080,1081],{"class":257},"\"$\"",[223,1083,1084],{"class":250},").",[223,1086,598],{"class":233},[223,1088,388],{"class":250},[223,1090,1092,1094,1096,1098,1100,1103,1106,1109,1111],{"class":225,"line":1091},66,[223,1093,516],{"class":229},[223,1095,449],{"class":250},[223,1097,463],{"class":229},[223,1099,467],{"class":466},[223,1101,1102],{"class":229}," &&",[223,1104,1105],{"class":229}," !",[223,1107,1108],{"class":250},"errors.",[223,1110,723],{"class":233},[223,1112,726],{"class":250},[223,1114,1116,1118,1121,1124,1126,1128,1131],{"class":225,"line":1115},67,[223,1117,446],{"class":229},[223,1119,1120],{"class":250}," err.",[223,1122,1123],{"class":233},"Error",[223,1125,601],{"class":250},[223,1127,604],{"class":229},[223,1129,1130],{"class":257}," \"BUSYGROUP Consumer Group name already exists\"",[223,1132,470],{"class":250},[223,1134,1136,1139],{"class":225,"line":1135},68,[223,1137,1138],{"class":229},"            return",[223,1140,1141],{"class":466}," nil\n",[223,1143,1145],{"class":225,"line":1144},69,[223,1146,499],{"class":250},[223,1148,1150,1153],{"class":225,"line":1149},70,[223,1151,1152],{"class":229},"        return",[223,1154,1155],{"class":250}," err\n",[223,1157,1159],{"class":225,"line":1158},71,[223,1160,570],{"class":250},[223,1162,1164,1167],{"class":225,"line":1163},72,[223,1165,1166],{"class":229},"    return",[223,1168,1141],{"class":466},[223,1170,1172],{"class":225,"line":1171},73,[223,1173,1001],{"class":250},[223,1175,1177],{"class":225,"line":1176},74,[223,1178,241],{"emptyLinePlaceholder":240},[223,1180,1182,1184,1186,1188,1190,1192,1194,1196,1198,1201,1204,1206,1209,1211,1213],{"class":225,"line":1181},75,[223,1183,346],{"class":229},[223,1185,871],{"class":233},[223,1187,410],{"class":250},[223,1189,1019],{"class":1018},[223,1191,1022],{"class":233},[223,1193,195],{"class":250},[223,1195,1027],{"class":233},[223,1197,532],{"class":250},[223,1199,1200],{"class":1018},"msg",[223,1202,1203],{"class":233}," redis",[223,1205,195],{"class":250},[223,1207,1208],{"class":233},"XMessage",[223,1210,1057],{"class":250},[223,1212,1060],{"class":229},[223,1214,470],{"class":250},[223,1216,1218,1221],{"class":225,"line":1217},76,[223,1219,1220],{"class":229},"    select",[223,1222,470],{"class":250},[223,1224,1226,1229,1232,1235,1238],{"class":225,"line":1225},77,[223,1227,1228],{"class":229},"    case",[223,1230,1231],{"class":229}," \u003C-",[223,1233,1234],{"class":250},"ctx.",[223,1236,1237],{"class":233},"Done",[223,1239,1240],{"class":250},"():\n",[223,1242,1244,1246,1248,1250],{"class":225,"line":1243},78,[223,1245,1152],{"class":229},[223,1247,595],{"class":250},[223,1249,598],{"class":233},[223,1251,388],{"class":250},[223,1253,1255,1258],{"class":225,"line":1254},79,[223,1256,1257],{"class":229},"    default",[223,1259,1260],{"class":250},":\n",[223,1262,1264],{"class":225,"line":1263},80,[223,1265,570],{"class":250},[223,1267,1269,1272,1274,1276,1279,1281,1284,1286,1288],{"class":225,"line":1268},81,[223,1270,1271],{"class":250},"    log.",[223,1273,479],{"class":233},[223,1275,410],{"class":250},[223,1277,1278],{"class":257},"\"message ",[223,1280,896],{"class":466},[223,1282,1283],{"class":257}," values=",[223,1285,487],{"class":466},[223,1287,490],{"class":257},[223,1289,1290],{"class":250},", msg.ID, msg.Values)\n",[223,1292,1294,1296],{"class":225,"line":1293},82,[223,1295,1166],{"class":229},[223,1297,1141],{"class":466},[223,1299,1301],{"class":225,"line":1300},83,[223,1302,1001],{"class":250},[15,1304,1305,1306,1309],{},"В реальном коде проверку ",[26,1307,1308],{},"BUSYGROUP"," лучше делать через typed helper или substring с осторожностью, потому что Redis возвращает server-side error string. Для учебного примера это достаточно.",[15,1311,1312],{},"Что ещё должно быть в production worker'е:",[52,1314,1315,1318,1321,1324,1327,1330],{},[55,1316,1317],{},"отдельный Redis client или pool budget для blocking reads;",[55,1319,1320],{},"bounded handler timeout, чтобы один зависший message не остановил consumer навсегда;",[55,1322,1323],{},"structured logs с stream, group, consumer, message_id и error_kind, но без чувствительного payload;",[55,1325,1326],{},"метрики read\u002Fhandle\u002Fack latency, failures, pending count и oldest pending age;",[55,1328,1329],{},"graceful shutdown, который перестаёт читать новые сообщения и ограниченно ждёт текущие handler'ы;",[55,1331,1332],{},"reclaimer для pending entries, иначе умерший worker оставит сообщения в PEL.",[32,1334],{},[35,1336,1338],{"id":1337},"production-patterns","Production patterns",[103,1340,1342],{"id":1341},"idempotency","Idempotency",[15,1344,1345],{},"Streams дают at-least-once обработку: сообщение может быть обработано больше одного раза.",[15,1347,1348],{},"Причины:",[52,1350,1351,1357,1360,1363],{},[55,1352,1353,1354,1356],{},"worker обработал событие, но умер до ",[26,1355,177],{},";",[55,1358,1359],{},"ack не дошёл до Redis;",[55,1361,1362],{},"сообщение reclaimed другим worker'ом;",[55,1364,1365],{},"приложение повторило обработку после timeout.",[15,1367,1368],{},"Значит handler должен быть idempotent.",[18,1370,1373],{"className":1371,"code":1372,"language":23,"meta":24},[21],"event: lesson_completed(user=42, lesson=redis-overview)\n\nПлохо:\n  INSERT progress row без unique constraint\n  INCR completed_count каждый раз\n\nЛучше:\n  INSERT ... ON CONFLICT DO NOTHING\n  INCR только если реально вставили новую completion\n",[26,1374,1372],{"__ignoreMap":24},[15,1376,1377],{},"Redis может помочь ключом идемпотентности:",[18,1379,1382],{"className":1380,"code":1381,"language":23,"meta":24},[21],"SET processed:event:1714471200000-0 1 NX EX 86400\n",[26,1383,1381],{"__ignoreMap":24},[15,1385,1386],{},"Но если результат обработки живёт в PostgreSQL, главный idempotency guard лучше держать там: unique constraint, transaction, outbox\u002Finbox table.",[32,1388],{},[103,1390,1392],{"id":1391},"dlq-like-pattern","DLQ-like pattern",[15,1394,1395],{},"В Redis Streams нет встроенной DLQ как отдельной сущности. Её делают соглашением:",[18,1397,1400],{"className":1398,"code":1399,"language":23,"meta":24},[21],"main stream: lesson-events\ndead stream: lesson-events:dlq\n",[26,1401,1399],{"__ignoreMap":24},[15,1403,1404],{},"Алгоритм:",[1406,1407,1408,1411,1414,1417],"ol",{},[55,1409,1410],{},"Worker читает сообщение.",[55,1412,1413],{},"Если обработка упала, не ack'ает сразу или увеличивает retry counter.",[55,1415,1416],{},"Reclaimer забирает pending старше N секунд.",[55,1418,1419,1420,1423],{},"После max attempts пишет событие в ",[26,1421,1422],{},"lesson-events:dlq"," и ack'ает исходное.",[15,1425,1426],{},"Важно сохранять причину ошибки:",[18,1428,1431],{"className":1429,"code":1430,"language":23,"meta":24},[21],"XADD lesson-events:dlq *\n  original_id 1714471200000-0\n  error \"invalid lesson slug\"\n  payload \"{...}\"\n  failed_at 2026-04-30T12:00:00Z\n",[26,1432,1430],{"__ignoreMap":24},[15,1434,1435],{},"DLQ без мониторинга бесполезна. На неё должен быть alert.",[32,1437],{},[103,1439,1441],{"id":1440},"retention","Retention",[15,1443,1444],{},"Stream растёт бесконечно, если его не ограничивать.",[18,1446,1449],{"className":1447,"code":1448,"language":23,"meta":24},[21],"XADD lesson-events MAXLEN ~ 100000 * type completed user_id 42\nXTRIM lesson-events MAXLEN ~ 100000\n",[26,1450,1448],{"__ignoreMap":24},[15,1452,1453,1456],{},[26,1454,1455],{},"~"," означает approximate trimming: быстрее, но не идеально точно.",[15,1458,1459],{},"Retention нужно выбирать по вопросу: сколько времени consumer может быть offline, чтобы потом догнать поток? Если worker может лежать сутки, stream должен хранить больше суток данных с запасом.",[15,1461,1462],{},"Осторожно с trimming и pending entries: если агрессивно обрезать stream, можно усложнить расследование и повторную обработку старых pending сообщений. Retention должен учитывать максимальный downtime consumer'а, DLQ-процесс и требования к audit\u002Fdebug.",[32,1464],{},[35,1466,1468],{"id":1467},"эксплуатация","Эксплуатация",[103,1470,1472],{"id":1471},"deployment","Deployment",[15,1474,1475],{},"Production Redis требует простых, но важных решений:",[52,1477,1478,1481,1487,1490,1493,1496,1499,1502],{},[55,1479,1480],{},"отдельные Redis instance для cache, locks и streams, если у них разные требования;",[55,1482,1483,1486],{},[26,1484,1485],{},"maxmemory"," и policy заданы явно;",[55,1488,1489],{},"persistence осознанно включена или выключена;",[55,1491,1492],{},"Redis не доступен из публичного интернета;",[55,1494,1495],{},"клиенты используют timeout'ы и connection pool;",[55,1497,1498],{},"heavy commands запрещены в request path;",[55,1500,1501],{},"backup и restore проверены, если данные важны;",[55,1503,1504],{},"rolling restart проверен на staging.",[32,1506],{},[103,1508,1510],{"id":1509},"monitoring","Monitoring",[15,1512,1513],{},"Смотреть нужно не только CPU.",[1515,1516,1517,1530],"table",{},[1518,1519,1520],"thead",{},[1521,1522,1523,1527],"tr",{},[1524,1525,1526],"th",{},"Метрика",[1524,1528,1529],{},"Что показывает",[1531,1532,1533,1547,1557,1567,1577,1587,1597,1607,1617,1627,1638],"tbody",{},[1521,1534,1535,1544],{},[1536,1537,1538,532,1541],"td",{},[26,1539,1540],{},"used_memory",[26,1542,1543],{},"used_memory_rss",[1536,1545,1546],{},"память и RSS",[1521,1548,1549,1554],{},[1536,1550,1551],{},[26,1552,1553],{},"mem_fragmentation_ratio",[1536,1555,1556],{},"fragmentation",[1521,1558,1559,1564],{},[1536,1560,1561],{},[26,1562,1563],{},"connected_clients",[1536,1565,1566],{},"количество клиентов",[1521,1568,1569,1574],{},[1536,1570,1571],{},[26,1572,1573],{},"blocked_clients",[1536,1575,1576],{},"blocking operations",[1521,1578,1579,1584],{},[1536,1580,1581],{},[26,1582,1583],{},"instantaneous_ops_per_sec",[1536,1585,1586],{},"текущий throughput",[1521,1588,1589,1594],{},[1536,1590,1591],{},[26,1592,1593],{},"keyspace_hits\u002Fmisses",[1536,1595,1596],{},"эффективность кеша",[1521,1598,1599,1604],{},[1536,1600,1601],{},[26,1602,1603],{},"evicted_keys",[1536,1605,1606],{},"eviction уже происходит",[1521,1608,1609,1614],{},[1536,1610,1611],{},[26,1612,1613],{},"expired_keys",[1536,1615,1616],{},"TTL expiration",[1521,1618,1619,1624],{},[1536,1620,1621],{},[26,1622,1623],{},"rejected_connections",[1536,1625,1626],{},"проблемы с maxclients",[1521,1628,1629,1635],{},[1536,1630,1631,1634],{},[26,1632,1633],{},"master_repl_offset",", replica lag",[1536,1636,1637],{},"отставание реплик",[1521,1639,1640,1643],{},[1536,1641,1642],{},"slowlog",[1536,1644,1645],{},"медленные команды",[15,1647,1648],{},"Для Streams дополнительно:",[52,1650,1651,1654,1657,1660,1663,1666],{},[55,1652,1653],{},"длина stream;",[55,1655,1656],{},"pending entries count;",[55,1658,1659],{},"возраст самого старого pending message;",[55,1661,1662],{},"retry count;",[55,1664,1665],{},"DLQ size;",[55,1667,1668],{},"consumer idle time.",[32,1670],{},[103,1672,1674],{"id":1673},"security","Security",[15,1676,1677],{},"Минимальный checklist:",[52,1679,1680,1683,1686,1689,1692,1695,1708,1711],{},[55,1681,1682],{},"bind только на private interface;",[55,1684,1685],{},"firewall\u002Fsecurity groups закрывают Redis от интернета;",[55,1687,1688],{},"включён ACL\u002Fpassword;",[55,1690,1691],{},"TLS, если сеть недоверенная;",[55,1693,1694],{},"разные пользователи ACL для разных приложений;",[55,1696,1697,1698,532,1701,532,1704,1707],{},"опасные команды ограничены (",[26,1699,1700],{},"FLUSHALL",[26,1702,1703],{},"CONFIG",[26,1705,1706],{},"DEBUG",");",[55,1709,1710],{},"секреты не лежат в git;",[55,1712,1713],{},"backup-файлы защищены так же, как база данных.",[15,1715,1716],{},"Redis часто содержит сессии, токены, персональные данные и внутренние события. Относитесь к нему как к production database, даже если он \"просто кеш\".",[15,1718,1719],{},"ACL стоит проектировать по роли: web API может иметь доступ к cache\u002Frate-limit ключам, worker - к stream keys, maintenance user - к ограниченному набору admin-команд. Один всемогущий пароль в переменной окружения для всех сервисов делает blast radius слишком большим.",[32,1721],{},[103,1723,1725],{"id":1724},"operability-и-runbooks","Operability и runbooks",[15,1727,1728],{},"Для Redis-инцидентов заранее опишите:",[52,1730,1731,1736,1739,1742,1745,1748],{},[55,1732,1733,1734,1356],{},"что делать при росте ",[26,1735,1603],{},[55,1737,1738],{},"как проверить top slow commands и slowlog;",[55,1740,1741],{},"как временно отключить необязательный кеш feature flag'ом;",[55,1743,1744],{},"как перезапустить worker без потери pending сообщений;",[55,1746,1747],{},"как восстановить Redis из backup, если persistence важна;",[55,1749,1750],{},"какие сценарии fail-open\u002Ffail-closed выбраны для API, rate limit и Streams.",[15,1752,1753,1754,195],{},"Runbook не должен быть длинным, но во время инцидента он экономит минуты и снижает шанс \"починить\" Redis командой ",[26,1755,1700],{},[32,1757],{},[103,1759,1761],{"id":1760},"production-checklist","Production checklist",[18,1763,1766],{"className":1764,"code":1765,"language":23,"meta":24},[21],"[ ] Для каждого ключа понятен owner и TTL\n[ ] Нет KEYS\u002FHGETALL\u002FSMEMBERS по большим структурам в request path\n[ ] maxmemory и eviction policy заданы явно\n[ ] Есть dashboard по memory, latency, evictions, hit ratio\n[ ] Slowlog включён и просматривается\n[ ] Клиенты используют context timeout\n[ ] Redis errors не валят read path кеша без необходимости\n[ ] Persistence выбрана осознанно\n[ ] Failover\u002Frestart проверен\n[ ] Streams имеют retention и DLQ-like процесс\n[ ] Handlers idempotent\n[ ] Redis закрыт от публичной сети\n[ ] ACL разделяют runtime, worker и maintenance доступ\n[ ] Есть runbook для Redis unavailable, high latency, evictions и stuck pending entries\n",[26,1767,1765],{"__ignoreMap":24},[32,1769],{},[35,1771,1773],{"id":1772},"вопросы-на-собеседовании","Вопросы на собеседовании",[52,1775,1776,1779,1782,1785,1788,1791,1794,1797,1800,1803,1806],{},[55,1777,1778],{},"Чем Redis Pub\u002FSub отличается от Streams?",[55,1780,1781],{},"Что произойдёт с Pub\u002FSub сообщением, если subscriber offline?",[55,1783,1784],{},"Что такое consumer group в Redis Streams?",[55,1786,1787],{},"Что такое Pending Entries List?",[55,1789,1790],{},"Почему обработчик Stream-сообщения должен быть idempotent?",[55,1792,1793],{},"Как сделать retry и DLQ-like процесс в Redis Streams?",[55,1795,1796],{},"Почему stream нужно trim'ить?",[55,1798,1799],{},"Какие Redis-метрики вы бы добавили на dashboard?",[55,1801,1802],{},"Какие security-настройки Redis обязательны в production?",[55,1804,1805],{},"Почему Pub\u002FSub invalidation всё равно требует TTL или version check?",[55,1807,1808],{},"Что нужно сделать с pending entries после смерти worker'а?",[32,1810],{},[35,1812,1814],{"id":1813},"практика","Практика",[1406,1816,1817,1834,1837,1840],{},[55,1818,1819,1820,1823,1824,532,1827,532,1830,1833],{},"Спроектируйте Stream ",[26,1821,1822],{},"lesson-events"," для событий ",[26,1825,1826],{},"lesson_started",[26,1828,1829],{},"lesson_completed",[26,1831,1832],{},"task_submitted",". Опишите поля событий.",[55,1835,1836],{},"Напишите worker, который читает consumer group и ack'ает сообщение только после успешной записи результата в PostgreSQL.",[55,1838,1839],{},"Опишите DLQ-like процесс: max attempts, где хранить ошибку, какие alert'ы нужны.",[55,1841,1842],{},"Составьте production checklist для Redis учебной платформы: cache, sessions, rate limits, streams.",[32,1844],{},[35,1846,1848],{"id":1847},"интерактивная-практика","Интерактивная практика",[1850,1851,1854,1857,1874],"quiz",{"answer":1852,"id":1853,"xp":684},"1","redis-streams-q1",[15,1855,1856],{},"Что произойдёт с Redis Pub\u002FSub сообщением, если subscriber offline?",[1858,1859,1860],"template",{"v-slot:options":24},[52,1861,1862,1865,1868,1871],{},[55,1863,1864],{},"Сообщение будет потеряно для этого subscriber",[55,1866,1867],{},"Redis сохранит его в Pending Entries List",[55,1869,1870],{},"Сообщение автоматически попадёт в DLQ",[55,1872,1873],{},"Redis превратит Pub\u002FSub канал в Stream",[1858,1875,1876],{"v-slot:explanation":24},[15,1877,1878],{},"Pub\u002FSub — fire-and-forget. Если subscriber не подключён в момент публикации, он не получит старое сообщение.",[1880,1881,1885,1888,2024],"predict",{"answer":1882,"id":1883,"xp":1884},"lost\\nstored","redis-streams-p1","15",[15,1886,1887],{},"Что выведет программа?",[1858,1889,1890],{"v-slot:code":24},[18,1891,1893],{"className":216,"code":1892,"language":218,"meta":24,"style":24},"package main\n\nimport \"fmt\"\n\nfunc deliveryModel(stream bool) string {\n    if stream {\n        return \"stored\"\n    }\n    return \"lost\"\n}\n\nfunc main() {\n    fmt.Println(deliveryModel(false))\n    fmt.Println(deliveryModel(true))\n}\n",[26,1894,1895,1901,1905,1917,1921,1941,1948,1955,1959,1966,1970,1974,1982,2003,2020],{"__ignoreMap":24},[223,1896,1897,1899],{"class":225,"line":226},[223,1898,230],{"class":229},[223,1900,234],{"class":233},[223,1902,1903],{"class":225,"line":237},[223,1904,241],{"emptyLinePlaceholder":240},[223,1906,1907,1909,1912,1915],{"class":225,"line":244},[223,1908,247],{"class":229},[223,1910,1911],{"class":257}," \"",[223,1913,1914],{"class":233},"fmt",[223,1916,264],{"class":257},[223,1918,1919],{"class":225,"line":254},[223,1920,241],{"emptyLinePlaceholder":240},[223,1922,1923,1925,1928,1930,1932,1935,1937,1939],{"class":225,"line":267},[223,1924,346],{"class":229},[223,1926,1927],{"class":233}," deliveryModel",[223,1929,410],{"class":250},[223,1931,1046],{"class":1018},[223,1933,1934],{"class":229}," bool",[223,1936,1057],{"class":250},[223,1938,662],{"class":229},[223,1940,470],{"class":250},[223,1942,1943,1945],{"class":225,"line":277},[223,1944,516],{"class":229},[223,1946,1947],{"class":250}," stream {\n",[223,1949,1950,1952],{"class":225,"line":287},[223,1951,1152],{"class":229},[223,1953,1954],{"class":257}," \"stored\"\n",[223,1956,1957],{"class":225,"line":297},[223,1958,570],{"class":250},[223,1960,1961,1963],{"class":225,"line":307},[223,1962,1166],{"class":229},[223,1964,1965],{"class":257}," \"lost\"\n",[223,1967,1968],{"class":225,"line":317},[223,1969,1001],{"class":250},[223,1971,1972],{"class":225,"line":322},[223,1973,241],{"emptyLinePlaceholder":240},[223,1975,1976,1978,1980],{"class":225,"line":332},[223,1977,346],{"class":229},[223,1979,349],{"class":233},[223,1981,352],{"class":250},[223,1983,1984,1987,1990,1992,1995,1997,2000],{"class":225,"line":338},[223,1985,1986],{"class":250},"    fmt.",[223,1988,1989],{"class":233},"Println",[223,1991,410],{"class":250},[223,1993,1994],{"class":233},"deliveryModel",[223,1996,410],{"class":250},[223,1998,1999],{"class":466},"false",[223,2001,2002],{"class":250},"))\n",[223,2004,2005,2007,2009,2011,2013,2015,2018],{"class":225,"line":343},[223,2006,1986],{"class":250},[223,2008,1989],{"class":233},[223,2010,410],{"class":250},[223,2012,1994],{"class":233},[223,2014,410],{"class":250},[223,2016,2017],{"class":466},"true",[223,2019,2002],{"class":250},[223,2021,2022],{"class":225,"line":355},[223,2023,1001],{"class":250},[1858,2025,2026],{"v-slot:hint":24},[15,2027,2028],{},"Streams записывают сообщения в log, Pub\u002FSub рассылает только текущим подписчикам.",[2030,2031,2035,2053,2198],"code-task",{"expected":2032,"id":2033,"xp":2034},"ack\\npending\\nclaim","redis-streams-ct1","20",[15,2036,2037,2038,2041,2042,2045,2046,2049,2050,195],{},"Реализуй ",[26,2039,2040],{},"StreamAction",": после успешной обработки нужен ",[26,2043,2044],{},"ack",", при ошибке сообщение остаётся ",[26,2047,2048],{},"pending",", а старое pending-сообщение другого worker можно ",[26,2051,2052],{},"claim",[1858,2054,2055],{"v-slot:template":24},[18,2056,2058],{"className":216,"code":2057,"language":218,"meta":24,"style":24},"package main\n\nimport \"fmt\"\n\nfunc StreamAction(processed bool, stalePending bool) string {\n    return \"todo\"\n}\n\nfunc main() {\n    fmt.Println(StreamAction(true, false))\n    fmt.Println(StreamAction(false, false))\n    fmt.Println(StreamAction(false, true))\n}\n",[26,2059,2060,2066,2070,2080,2084,2111,2118,2122,2126,2134,2154,2174,2194],{"__ignoreMap":24},[223,2061,2062,2064],{"class":225,"line":226},[223,2063,230],{"class":229},[223,2065,234],{"class":233},[223,2067,2068],{"class":225,"line":237},[223,2069,241],{"emptyLinePlaceholder":240},[223,2071,2072,2074,2076,2078],{"class":225,"line":244},[223,2073,247],{"class":229},[223,2075,1911],{"class":257},[223,2077,1914],{"class":233},[223,2079,264],{"class":257},[223,2081,2082],{"class":225,"line":254},[223,2083,241],{"emptyLinePlaceholder":240},[223,2085,2086,2088,2091,2093,2096,2098,2100,2103,2105,2107,2109],{"class":225,"line":267},[223,2087,346],{"class":229},[223,2089,2090],{"class":233}," StreamAction",[223,2092,410],{"class":250},[223,2094,2095],{"class":1018},"processed",[223,2097,1934],{"class":229},[223,2099,532],{"class":250},[223,2101,2102],{"class":1018},"stalePending",[223,2104,1934],{"class":229},[223,2106,1057],{"class":250},[223,2108,662],{"class":229},[223,2110,470],{"class":250},[223,2112,2113,2115],{"class":225,"line":277},[223,2114,1166],{"class":229},[223,2116,2117],{"class":257}," \"todo\"\n",[223,2119,2120],{"class":225,"line":287},[223,2121,1001],{"class":250},[223,2123,2124],{"class":225,"line":297},[223,2125,241],{"emptyLinePlaceholder":240},[223,2127,2128,2130,2132],{"class":225,"line":307},[223,2129,346],{"class":229},[223,2131,349],{"class":233},[223,2133,352],{"class":250},[223,2135,2136,2138,2140,2142,2144,2146,2148,2150,2152],{"class":225,"line":317},[223,2137,1986],{"class":250},[223,2139,1989],{"class":233},[223,2141,410],{"class":250},[223,2143,2040],{"class":233},[223,2145,410],{"class":250},[223,2147,2017],{"class":466},[223,2149,532],{"class":250},[223,2151,1999],{"class":466},[223,2153,2002],{"class":250},[223,2155,2156,2158,2160,2162,2164,2166,2168,2170,2172],{"class":225,"line":322},[223,2157,1986],{"class":250},[223,2159,1989],{"class":233},[223,2161,410],{"class":250},[223,2163,2040],{"class":233},[223,2165,410],{"class":250},[223,2167,1999],{"class":466},[223,2169,532],{"class":250},[223,2171,1999],{"class":466},[223,2173,2002],{"class":250},[223,2175,2176,2178,2180,2182,2184,2186,2188,2190,2192],{"class":225,"line":332},[223,2177,1986],{"class":250},[223,2179,1989],{"class":233},[223,2181,410],{"class":250},[223,2183,2040],{"class":233},[223,2185,410],{"class":250},[223,2187,1999],{"class":466},[223,2189,532],{"class":250},[223,2191,2017],{"class":466},[223,2193,2002],{"class":250},[223,2195,2196],{"class":225,"line":338},[223,2197,1001],{"class":250},[1858,2199,2200],{"v-slot:hints":24},[52,2201,2202,2207,2210],{},[55,2203,2204,2206],{},[26,2205,177],{}," делается после успешного side effect.",[55,2208,2209],{},"Неуспешное сообщение остаётся в PEL.",[55,2211,2212],{},"Старые pending entries можно забрать через claim-процесс.",[2214,2215,2216],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":24,"searchDepth":237,"depth":237,"links":2218},[2219,2220,2226,2231,2238,2239,2240],{"id":37,"depth":237,"text":38},{"id":100,"depth":237,"text":101,"children":2221},[2222,2223,2224,2225],{"id":105,"depth":244,"text":106},{"id":139,"depth":244,"text":140},{"id":166,"depth":244,"text":167},{"id":209,"depth":244,"text":210},{"id":1337,"depth":237,"text":1338,"children":2227},[2228,2229,2230],{"id":1341,"depth":244,"text":1342},{"id":1391,"depth":244,"text":1392},{"id":1440,"depth":244,"text":1441},{"id":1467,"depth":237,"text":1468,"children":2232},[2233,2234,2235,2236,2237],{"id":1471,"depth":244,"text":1472},{"id":1509,"depth":244,"text":1510},{"id":1673,"depth":244,"text":1674},{"id":1724,"depth":244,"text":1725},{"id":1760,"depth":244,"text":1761},{"id":1772,"depth":237,"text":1773},{"id":1813,"depth":237,"text":1814},{"id":1847,"depth":237,"text":1848},1781022067125]