Skip to content

Commit 161d078

Browse files
Alexey PortnovAlexey Portnov
authored andcommitted
Исправил таймауты API и потерю CDR при сбоях. Fixes #44
- Увеличил таймауты HTTP-запросов (5→15 сек), добавил connect_timeout=5 - Добавил retry с backoff (3 попытки) при ConnectException в ClientHTTP - Откат offset при неудаче addCalls, после 5 провалов — пропуск батча - Сохранение провальных CDR linkedids в таблицу m_ModuleAmoFailedCdr
1 parent 67f1aea commit 161d078

File tree

4 files changed

+161
-66
lines changed

4 files changed

+161
-66
lines changed

Lib/ClientHTTP.php

Lines changed: 56 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -35,30 +35,13 @@ class ClientHTTP
3535
public static function sendHttpPostRequest(string $url, array $params, array $headers=[]):PBXAmoResult{
3636
$client = new GuzzleHttp\Client();
3737
$options = [
38-
'timeout' => 5,
39-
'http_errors' => false,
40-
'headers' => $headers,
41-
'json' => $params,
38+
'timeout' => 15,
39+
'connect_timeout' => 5,
40+
'http_errors' => false,
41+
'headers' => $headers,
42+
'json' => $params,
4243
];
43-
$message = '';
44-
$resultHttp = null;
45-
try {
46-
$resultHttp = $client->request('POST', $url, $options);
47-
$code = $resultHttp->getStatusCode();
48-
}catch (GuzzleHttp\Exception\ConnectException $e ){
49-
$message = $e->getMessage();
50-
51-
$errorMessage = "Error: " . $e->getMessage() . ',';
52-
$errorMessage .= "Request URL: " . $e->getRequest()->getUri() . ',';
53-
$errorMessage .= "Request Method: " . $e->getRequest()->getMethod() . ',';
54-
Util::sysLogMsg('ModuleAmoCrm', "ConnectException: ".$errorMessage);
55-
$code = 0;
56-
} catch (GuzzleException $e) {
57-
$message = $e->getMessage();
58-
Util::sysLogMsg('ModuleAmoCrm', "GuzzleException");
59-
$code = 0;
60-
}
61-
return self::parseResponse($resultHttp, $message, $code);
44+
return self::executeWithRetry($client, 'POST', $url, $options);
6245
}
6346

6447
/**
@@ -71,29 +54,13 @@ public static function sendHttpPostRequest(string $url, array $params, array $he
7154
public static function sendHttpPatchRequest(string $url, array $params, array $headers=[]):PBXAmoResult{
7255
$client = new GuzzleHttp\Client();
7356
$options = [
74-
'timeout' => 5,
75-
'http_errors' => false,
76-
'headers' => $headers,
77-
'json' => $params,
57+
'timeout' => 15,
58+
'connect_timeout' => 5,
59+
'http_errors' => false,
60+
'headers' => $headers,
61+
'json' => $params,
7862
];
79-
$message = '';
80-
$resultHttp = null;
81-
try {
82-
$resultHttp = $client->request('PATCH', $url, $options);
83-
$code = $resultHttp->getStatusCode();
84-
}catch (GuzzleHttp\Exception\ConnectException $e ){
85-
$message = $e->getMessage();
86-
$errorMessage = "Error: " . $e->getMessage() . ',';
87-
$errorMessage .= "Request URL: " . $e->getRequest()->getUri() . ',';
88-
$errorMessage .= "Request Method: " . $e->getRequest()->getMethod() . ',';
89-
Util::sysLogMsg('ModuleAmoCrm', "ConnectException: ".$errorMessage);
90-
$code = 0;
91-
} catch (GuzzleException $e) {
92-
$message = $e->getMessage();
93-
Util::sysLogMsg('ModuleAmoCrm', "GuzzleException");
94-
$code = 0;
95-
}
96-
return self::parseResponse($resultHttp, $message, $code);
63+
return self::executeWithRetry($client, 'PATCH', $url, $options);
9764
}
9865

9966
/**
@@ -109,26 +76,52 @@ public static function sendHttpGetRequest(string $url, array $params, array $hea
10976
}
11077
$client = new GuzzleHttp\Client();
11178
$options = [
112-
'timeout' => 8,
113-
'http_errors' => false,
114-
'headers' => $headers,
79+
'timeout' => 15,
80+
'connect_timeout' => 5,
81+
'http_errors' => false,
82+
'headers' => $headers,
11583
];
116-
$message = '';
84+
return self::executeWithRetry($client, 'GET', $url, $options);
85+
}
86+
87+
/**
88+
* Выполнение HTTP-запроса с retry при ConnectException (таймауты, сетевые ошибки).
89+
* До 3 попыток с линейным backoff (2, 4, 6 сек).
90+
* @param GuzzleHttp\Client $client
91+
* @param string $method
92+
* @param string $url
93+
* @param array $options
94+
* @return PBXAmoResult
95+
*/
96+
private static function executeWithRetry(GuzzleHttp\Client $client, string $method, string $url, array $options):PBXAmoResult
97+
{
98+
$maxRetries = 3;
99+
$retryDelay = 2;
100+
$message = '';
117101
$resultHttp = null;
118-
try {
119-
$resultHttp = $client->request('GET', $url, $options);
120-
$code = $resultHttp->getStatusCode();
121-
}catch (GuzzleHttp\Exception\ConnectException $e ){
122-
$message = $e->getMessage();
123-
$errorMessage = "Error: " . $e->getMessage() . ',';
124-
$errorMessage .= "Request URL: " . $e->getRequest()->getUri() . ',';
125-
$errorMessage .= "Request Method: " . $e->getRequest()->getMethod() . ',';
126-
Util::sysLogMsg('ModuleAmoCrm', "ConnectException: ".$errorMessage);
127-
$code = 0;
128-
} catch (GuzzleException $e) {
129-
$message = $e->getMessage();
130-
Util::sysLogMsg('ModuleAmoCrm', "GuzzleException");
131-
$code = 0;
102+
$code = 0;
103+
for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
104+
try {
105+
$resultHttp = $client->request($method, $url, $options);
106+
$code = $resultHttp->getStatusCode();
107+
break;
108+
} catch (GuzzleHttp\Exception\ConnectException $e) {
109+
$message = $e->getMessage();
110+
$errorMessage = "Error: " . $e->getMessage() . ',';
111+
$errorMessage .= "Request URL: " . $e->getRequest()->getUri() . ',';
112+
$errorMessage .= "Request Method: " . $e->getRequest()->getMethod() . ',';
113+
$errorMessage .= "Attempt: $attempt/$maxRetries";
114+
Util::sysLogMsg('ModuleAmoCrm', "ConnectException: " . $errorMessage);
115+
if ($attempt < $maxRetries) {
116+
sleep($retryDelay * $attempt);
117+
}
118+
$code = 0;
119+
} catch (GuzzleException $e) {
120+
$message = $e->getMessage();
121+
Util::sysLogMsg('ModuleAmoCrm', "GuzzleException: " . $message);
122+
$code = 0;
123+
break;
124+
}
132125
}
133126
return self::parseResponse($resultHttp, $message, $code);
134127
}

Models/ModuleAmoFailedCdr.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
/**
3+
* Copyright © MIKO LLC - All Rights Reserved
4+
* Unauthorized copying of this file, via any medium is strictly prohibited
5+
* Proprietary and confidential
6+
* Written by Alexey Portnov, 2 2026
7+
*/
8+
9+
namespace Modules\ModuleAmoCrm\Models;
10+
use MikoPBX\Modules\Models\ModulesModelsBase;
11+
12+
/**
13+
* Хранение провальных CDR, которые не удалось отправить в AmoCRM.
14+
*
15+
* @Indexes(
16+
* [name='linkedid', columns=['linkedid'], type=''],
17+
* [name='failedAt', columns=['failedAt'], type='']
18+
* )
19+
*/
20+
class ModuleAmoFailedCdr extends ModulesModelsBase
21+
{
22+
/**
23+
* @Primary
24+
* @Identity
25+
* @Column(type="integer", nullable=false)
26+
*/
27+
public $id;
28+
29+
/**
30+
* @Column(type="string", nullable=true)
31+
*/
32+
public $linkedid;
33+
34+
/**
35+
* @Column(type="integer", nullable=true)
36+
*/
37+
public $failedAt = 0;
38+
39+
/**
40+
* @Column(type="string", nullable=true)
41+
*/
42+
public $reason = '';
43+
44+
/**
45+
* @param $calledModelObject
46+
* @return void
47+
*/
48+
public static function getDynamicRelations(&$calledModelObject): void
49+
{
50+
}
51+
52+
public function initialize(): void
53+
{
54+
$this->setSource('m_ModuleAmoFailedCdr');
55+
parent::initialize();
56+
}
57+
}

bin/AmoCdrDaemon.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class AmoCdrDaemon extends WorkerBase
6565
private array $newTasks = [];
6666
private array $incompleteAnswered = [];
6767
private array $createdLeads = []; // Кэш создана ли сделка для звонка. 1 звонок = 1 сделка
68+
private int $addCallsFailCount = 0; // Счётчик последовательных неудач addCalls
69+
private const MAX_ADD_CALLS_FAILURES = 5; // Максимум попыток перед пропуском батча
6870

6971
// ВИДЫ ЗВОНКОВ.
7072
// Входящие
@@ -450,7 +452,24 @@ private function cdrSync():void
450452
////
451453
// Прикрепление звонков к сущностям.
452454
////
453-
$this->addCalls($calls, $callCounter);
455+
$callsOk = $this->addCalls($calls, $callCounter);
456+
if(!$callsOk){
457+
$this->addCallsFailCount++;
458+
if($this->addCallsFailCount >= self::MAX_ADD_CALLS_FAILURES){
459+
// Превышен лимит попыток — пропускаем батч, фиксируем в БД и в лог.
460+
$failedIds = array_unique(array_keys($this->cdrRows));
461+
$this->logger->writeError("addCalls failed $this->addCallsFailCount times in a row, skipping batch. Lost linkedids: " . implode(', ', $failedIds));
462+
ConnectorDb::invoke('saveFailedCdr', [$failedIds, 'addCalls timeout after ' . self::MAX_ADD_CALLS_FAILURES . ' attempts'], false);
463+
$this->addCallsFailCount = 0;
464+
// offset НЕ откатываем — пропускаем батч
465+
}else{
466+
$this->offset = $oldOffset;
467+
$this->logger->writeError("addCalls failed (attempt $this->addCallsFailCount/" . self::MAX_ADD_CALLS_FAILURES . "), offset rolled back to $oldOffset");
468+
return;
469+
}
470+
}else{
471+
$this->addCallsFailCount = 0;
472+
}
454473

455474
if($oldOffset !== $this->offset){
456475
ConnectorDb::invoke('saveNewSettings', [['offsetCdr' => $this->offset]]);
@@ -549,15 +568,15 @@ private function setMissedData(string $linkedId, string $start, ?string $amoUser
549568
* @param $callCounter
550569
* @return void
551570
*/
552-
private function addCalls($calls, $callCounter):void
571+
private function addCalls($calls, $callCounter):bool
553572
{
554573
$calls = array_merge(... array_values($calls));
555574
if(!empty($calls)){
556575
$this->logger->writeInfo($calls, "CDR synchronization. Step 1 Count: ".count($calls));
557576
$calls = $this->cleanCalls($calls, $callCounter);
558577
}
559578
if(empty($calls)){
560-
return;
579+
return true;
561580
}
562581
$this->logger->writeInfo($calls, "CDR synchronization. Step 2. Count: ".count($calls));
563582
$ids = '';
@@ -573,6 +592,7 @@ private function addCalls($calls, $callCounter):void
573592
$callsArray[$entity_type][] = $call;
574593
}
575594
unset($call, $calls);
595+
$allSuccess = true;
576596
foreach ($callsArray as $entity_type => $callsData){
577597
if(empty($callsData)){
578598
continue;
@@ -581,7 +601,12 @@ private function addCalls($calls, $callCounter):void
581601
$result = WorkerAmoHTTP::invokeAmoApi('addCalls', [$callsData, $entity_type]);
582602
$this->logger->writeInfo($callsData, "Create calls (REQ): $ids");
583603
$this->logger->writeInfo($result, "Create calls (RES): $ids");
604+
if(!$result->success){
605+
$this->logger->writeError("Failed to add calls for $entity_type, offset will be rolled back");
606+
$allSuccess = false;
607+
}
584608
}
609+
return $allSuccess;
585610
}
586611

587612
/**

bin/ConnectorDb.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Modules\ModuleAmoCrm\Models\ModuleAmoPhones;
3636
use Modules\ModuleAmoCrm\Models\ModuleAmoPipeLines;
3737
use Modules\ModuleAmoCrm\Models\ModuleAmoUsers;
38+
use Modules\ModuleAmoCrm\Models\ModuleAmoFailedCdr;
3839
use Throwable;
3940
use Phalcon\Mvc\Model\Manager;
4041

@@ -1109,6 +1110,25 @@ public function saveEntitySettingsAction($data):array
11091110
}
11101111

11111112

1113+
/**
1114+
* Сохранение провального CDR в БД.
1115+
* @param array $linkedIds
1116+
* @param string $reason
1117+
* @return bool
1118+
*/
1119+
public function saveFailedCdr(array $linkedIds, string $reason = ''):bool
1120+
{
1121+
$result = true;
1122+
$now = time();
1123+
foreach ($linkedIds as $linkedId) {
1124+
$record = new ModuleAmoFailedCdr();
1125+
$record->linkedid = $linkedId;
1126+
$record->failedAt = $now;
1127+
$record->reason = $reason;
1128+
$result = min($record->save(), $result);
1129+
}
1130+
return $result;
1131+
}
11121132
}
11131133

11141134
if(isset($argv) && count($argv) !== 1

0 commit comments

Comments
 (0)