* [PATCH v2 0/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching.
@ 2026-01-05 10:38 Mengshi Wu
2026-01-05 10:38 ` [PATCH v2 1/1] " Mengshi Wu
0 siblings, 1 reply; 6+ messages in thread
From: Mengshi Wu @ 2026-01-05 10:38 UTC (permalink / raw)
To: luiz.dentz
Cc: linux-bluetooth, shuai.zhang, cheng.jiang, chezhou, wei.deng,
yiboz, Mengshi Wu
Changes from v1:
- Implement automatic recovery when ATT_ECODE_DB_OUT_OF_SYNC error is
received from the remote device.
- Link to v1
https://lore.kernel.org/all/20251208101915.247459-1-mengshi.wu@oss.qualcomm.com/
Mengshi Wu (1):
gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT
caching.
src/shared/gatt-client.c | 376 +++++++++++++++++++++++++++++++++++++-
src/shared/gatt-helpers.c | 16 ++
src/shared/gatt-helpers.h | 3 +
3 files changed, 392 insertions(+), 3 deletions(-)
--
2.34.1
^ permalink raw reply [flat|nested] 6+ messages in thread* [PATCH v2 1/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching. 2026-01-05 10:38 [PATCH v2 0/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching Mengshi Wu @ 2026-01-05 10:38 ` Mengshi Wu 2026-01-05 10:54 ` bluez.test.bot 2026-01-05 19:21 ` [PATCH v2 1/1] " Luiz Augusto von Dentz 0 siblings, 2 replies; 6+ messages in thread From: Mengshi Wu @ 2026-01-05 10:38 UTC (permalink / raw) To: luiz.dentz Cc: linux-bluetooth, shuai.zhang, cheng.jiang, chezhou, wei.deng, yiboz, Mengshi Wu Implement automatic recovery when ATT_ECODE_DB_OUT_OF_SYNC error is received from the remote device. The recovery mechanism: - Detects DB_OUT_OF_SYNC errors during GATT operations - Extracts affected handles from the original request PDU - Checks if Service Changed indications overlap with those handles - Verifies database consistency using Database Hash characteristic - Automatically retries the original request if DB is consistent - Automatically retries the original request if handle is not affected This may prevent some application-level failures when the GATT database changes on the remote device. Signed-off-by: Mengshi Wu <mengshi.wu@oss.qualcomm.com> --- src/shared/gatt-client.c | 376 +++++++++++++++++++++++++++++++++++++- src/shared/gatt-helpers.c | 16 ++ src/shared/gatt-helpers.h | 3 + 3 files changed, 392 insertions(+), 3 deletions(-) diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c index f8ebab3fa..22b6c5d1d 100644 --- a/src/shared/gatt-client.c +++ b/src/shared/gatt-client.c @@ -41,6 +41,9 @@ gatt_log(_client, "[%p] %s:%s() " _format, _client, __FILE__, \ __func__, ## arg) +#define DB_OUT_OF_SYNC_HOLD_ON true +#define DB_OUT_OF_SYNC_GO_ON false + struct ready_cb { bt_gatt_client_callback_t callback; bt_gatt_client_destroy_func_t destroy; @@ -114,6 +117,9 @@ struct bt_gatt_client { struct bt_gatt_request *discovery_req; unsigned int mtu_req_id; + + /* Pending DB out of sync handling */ + struct db_out_of_sync_data *pending_db_sync; }; struct request { @@ -126,8 +132,31 @@ struct request { unsigned int att_id; void *data; void (*destroy)(void *); + + /* For DB_OUT_OF_SYNC recovery capability */ + uint8_t *sent_pdu; + uint16_t sent_pdu_len; + uint8_t sent_opcode; +}; + +struct db_out_of_sync_data { + struct bt_gatt_client *client; + struct request *original_req; + uint8_t opcode; + uint8_t *pdu; + uint16_t pdu_len; + bt_att_response_func_t att_callback; + uint16_t *handles; + uint8_t num_handles; + bool handle_overlaps; + bool svc_changed_arrived; + /* Store original error PDU */ + struct bt_att_pdu_error_rsp error_pdu; }; +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data); +static void call_original_callback_with_error(struct db_out_of_sync_data *data); + static struct request *request_ref(struct request *req) { __sync_fetch_and_add(&req->ref_count, 1); @@ -210,6 +239,7 @@ static void request_unref(void *data) notify_client_idle(client); } + free(req->sent_pdu); free(req); } @@ -1968,11 +1998,272 @@ fail: "Failed to initiate service discovery after Service Changed"); } +static void db_out_of_sync_recover(struct bt_gatt_client *client) +{ + struct db_out_of_sync_data *pending = client->pending_db_sync; + unsigned int new_att_id = 0; + + assert(pending); + + new_att_id = bt_att_send(client->att, pending->opcode, pending->pdu, + pending->pdu_len, pending->att_callback, + request_ref(pending->original_req), + request_unref); + if (new_att_id) + pending->original_req->att_id = new_att_id; + else + call_original_callback_with_error(pending); + client->pending_db_sync = NULL; + db_out_of_sync_data_free(pending); +} + +static void db_hash_check_read_cb(bool success, uint8_t att_ecode, + struct bt_gatt_result *result, + void *user_data) +{ + struct bt_gatt_client *client = user_data; + struct db_out_of_sync_data *pending = client->pending_db_sync; + const uint8_t *local_hash = NULL, *remote_hash; + struct gatt_db_attribute *hash_attr = NULL; + struct service_changed_op *op; + struct bt_gatt_iter iter; + bt_uuid_t uuid; + uint16_t handle, len; + + assert(pending); + + if (pending->svc_changed_arrived) { + if (!pending->handle_overlaps) { + /* No overlap - resend original request */ + DBG(client, "Service changed range doesn't overlap"); + db_out_of_sync_recover(client); + } + + return; + } + + /* If read failed, fall back to full re-discovery */ + if (!success) + goto trigger_rediscovery; + + if (!result || !bt_gatt_iter_init(&iter, result)) + goto trigger_rediscovery; + + if (!bt_gatt_iter_next_read_by_type(&iter, &handle, + &len, &remote_hash)) + goto trigger_rediscovery; + + if (len != 16) + goto trigger_rediscovery; + + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); + gatt_db_find_by_type(client->db, 0x0001, 0xffff, &uuid, + get_first_attribute, &hash_attr); + + if (hash_attr) { + gatt_db_attribute_read(hash_attr, 0, BT_ATT_OP_READ_REQ, NULL, + db_hash_read_value_cb, &local_hash); + } + + /* If hashes match, no need to trigger re-discovery */ + if (local_hash && !memcmp(local_hash, remote_hash, 16)) { + db_out_of_sync_recover(client); + return; + } + + DBG(client, "DB Hash mismatch: triggering re-discovery"); + +trigger_rediscovery: + call_original_callback_with_error(pending); + client->pending_db_sync = NULL; + db_out_of_sync_data_free(pending); + + process_service_changed(client, 0x0001, 0xffff); +} + +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data) +{ + if (!data) + return; + + if (data->original_req) + request_unref(data->original_req); + + free(data->pdu); + free(data->handles); + free(data); +} + +static bool check_handle_overlap(uint16_t *handles, uint8_t num_handles, + uint16_t start, uint16_t end) +{ + uint8_t i; + + if (!handles) + return true; + + for (i = 0; i < num_handles; i++) { + if (handles[i] >= start && handles[i] <= end) + return true; + } + + return false; +} + +static uint8_t extract_handles_from_pdu(uint8_t opcode, const uint8_t *pdu, + uint16_t pdu_len, uint16_t **handles) +{ + uint8_t num_handles = 0; + uint16_t *handle_array; + uint16_t i; + + switch (opcode) { + case BT_ATT_OP_READ_REQ: + case BT_ATT_OP_READ_BLOB_REQ: + case BT_ATT_OP_WRITE_REQ: + case BT_ATT_OP_WRITE_CMD: + case BT_ATT_OP_PREP_WRITE_REQ: + /* Single handle at offset 0 */ + num_handles = 1; + handle_array = malloc(sizeof(uint16_t)); + if (handle_array) + handle_array[0] = get_le16(pdu); + break; + + case BT_ATT_OP_READ_MULT_REQ: + case BT_ATT_OP_READ_MULT_VL_REQ: + /* Multiple handles, 2 bytes each */ + num_handles = pdu_len / 2; + handle_array = malloc(num_handles * sizeof(uint16_t)); + if (handle_array) { + for (i = 0; i < num_handles; i++) + handle_array[i] = get_le16(pdu + (i * 2)); + } + break; + + default: + return 0; + } + + if (!handle_array) + return 0; + + *handles = handle_array; + return num_handles; +} + +static void call_original_callback_with_error(struct db_out_of_sync_data *data) +{ + struct request *req = data->original_req; + + if (!req || !data->att_callback) + return; + + data->att_callback(BT_ATT_OP_ERROR_RSP, &(data->error_pdu), + sizeof(struct bt_att_pdu_error_rsp), req); +} + +static bool process_db_out_of_sync(struct bt_gatt_client *client, + uint8_t att_ecode, const void *error_pdu, + struct request *req, + bt_att_response_func_t callback) +{ + struct db_out_of_sync_data *pending; + struct service_changed_op *op; + struct bt_gatt_request *gatt_req_op = client->discovery_req; + const struct bt_att_pdu_error_rsp *epdu = error_pdu; + bt_uuid_t uuid; + unsigned int new_att_id = 0; + + if (att_ecode != BT_ATT_ERROR_DB_OUT_OF_SYNC) + return DB_OUT_OF_SYNC_GO_ON; + + /* Check if we already have a pending operation */ + if (client->pending_db_sync) + return DB_OUT_OF_SYNC_GO_ON; + + /* Only handle if we have the necessary request info */ + if (!req || !req->sent_pdu || !callback) + goto trigger_rediscovery; + + /* Create pending structure */ + pending = new0(struct db_out_of_sync_data, 1); + if (!pending) + goto trigger_rediscovery; + + pending->client = client; + pending->original_req = request_ref(req); + pending->att_callback = callback; + pending->opcode = req->sent_opcode; + pending->pdu_len = req->sent_pdu_len; + + /* Copy PDU */ + pending->pdu = malloc(pending->pdu_len); + if (!pending->pdu) { + db_out_of_sync_data_free(pending); + goto trigger_rediscovery; + } + memcpy(pending->pdu, req->sent_pdu, pending->pdu_len); + + /* Store original error PDU */ + memcpy(&(pending->error_pdu), error_pdu, + sizeof(struct bt_att_pdu_error_rsp)); + + /* Extract handles from PDU */ + pending->num_handles = + extract_handles_from_pdu(pending->opcode, pending->pdu, + pending->pdu_len, &pending->handles); + if (!pending->num_handles) { + db_out_of_sync_data_free(pending); + goto trigger_rediscovery; + } + + /* Store pending operation */ + client->pending_db_sync = pending; + + /* Initiate hash read */ + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); + + if (bt_gatt_read_by_type(client->att, 0x0001, 0xffff, &uuid, + db_hash_check_read_cb, client, NULL)) { + return DB_OUT_OF_SYNC_HOLD_ON; + } + + client->pending_db_sync = NULL; + db_out_of_sync_data_free(pending); + +trigger_rediscovery: + + if (client->in_svc_chngd) { + if (client->discovery_req && req && req->sent_pdu && callback && + (epdu->handle != 0) && gatt_req_op && + (bt_gatt_request_get_start_handle(gatt_req_op) > + epdu->handle || + bt_gatt_request_get_end_handle(gatt_req_op) < + epdu->handle)) { + new_att_id = bt_att_send(client->att, req->sent_opcode, + req->sent_pdu, + req->sent_pdu_len, callback, + request_ref(req), + request_unref); + if (new_att_id) { + req->att_id = new_att_id; + return DB_OUT_OF_SYNC_HOLD_ON; + } + } + return DB_OUT_OF_SYNC_GO_ON; + } + + process_service_changed(client, 0x0001, 0xffff); + return DB_OUT_OF_SYNC_GO_ON; +} + static void service_changed_cb(uint16_t value_handle, const uint8_t *value, uint16_t length, void *user_data) { struct bt_gatt_client *client = user_data; struct service_changed_op *op; + struct db_out_of_sync_data *pending; uint16_t start, end; if (length != 4) @@ -1990,6 +2281,14 @@ static void service_changed_cb(uint16_t value_handle, const uint8_t *value, DBG(client, "Service Changed received - start: 0x%04x end: 0x%04x", start, end); + /* Check if there's a pending DB out of sync operation */ + pending = client->pending_db_sync; + if (pending) { + pending->svc_changed_arrived = true; + pending->handle_overlaps = check_handle_overlap(pending->handles, + pending->num_handles, start, end); + } + if (!client->in_svc_chngd) { process_service_changed(client, start, end); return; @@ -2332,6 +2631,10 @@ static void att_disconnect_cb(int err, void *user_data) client->disc_id = 0; + /* Cleanup pending DB out of sync operation */ + db_out_of_sync_data_free(client->pending_db_sync); + client->pending_db_sync = NULL; + bt_att_unref(client->att); client->att = NULL; @@ -2712,10 +3015,15 @@ static void read_multiple_cb(uint8_t opcode, const void *pdu, uint16_t length, (!pdu && length)) { success = false; - if (opcode == BT_ATT_OP_ERROR_RSP) + if (opcode == BT_ATT_OP_ERROR_RSP) { att_ecode = process_error(pdu, length); - else + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + read_multiple_cb)) + return; + } else { att_ecode = 0; + } pdu = NULL; length = 0; @@ -2799,6 +3107,13 @@ unsigned int bt_gatt_client_read_multiple(struct bt_gatt_client *client, BT_GATT_CHRC_CLI_FEAT_EATT ? BT_ATT_OP_READ_MULT_VL_REQ : BT_ATT_OP_READ_MULT_REQ; + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = opcode; + req->sent_pdu_len = num_handles * 2; + req->sent_pdu = malloc(req->sent_pdu_len); + if (req->sent_pdu) + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); + req->att_id = bt_att_send(client->att, opcode, pdu, num_handles * 2, read_multiple_cb, req, request_unref); @@ -2867,6 +3182,10 @@ static void read_long_cb(uint8_t opcode, const void *pdu, if (opcode == BT_ATT_OP_ERROR_RSP) { success = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + read_long_cb)) + return; goto done; } @@ -2975,6 +3294,13 @@ unsigned int bt_gatt_client_read_long_value(struct bt_gatt_client *client, att_op = BT_ATT_OP_READ_REQ; } + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = att_op; + req->sent_pdu_len = pdu_len; + req->sent_pdu = malloc(req->sent_pdu_len); + if (req->sent_pdu) + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); + req->att_id = bt_att_send(client->att, att_op, pdu, pdu_len, read_long_cb, req, request_unref); @@ -3053,6 +3379,9 @@ static void write_cb(uint8_t opcode, const void *pdu, uint16_t length, if (opcode == BT_ATT_OP_ERROR_RSP) { success = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, write_cb)) + return; goto done; } @@ -3096,6 +3425,13 @@ unsigned int bt_gatt_client_write_value(struct bt_gatt_client *client, put_le16(value_handle, pdu); memcpy(pdu + 2, value, length); + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = BT_ATT_OP_WRITE_REQ; + req->sent_pdu_len = 2 + length; + req->sent_pdu = malloc(req->sent_pdu_len); + if (req->sent_pdu) + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); + req->att_id = bt_att_send(client->att, BT_ATT_OP_WRITE_REQ, pdu, 2 + length, write_cb, req, @@ -3216,6 +3552,10 @@ static void execute_write_cb(uint8_t opcode, const void *pdu, uint16_t length, if (opcode == BT_ATT_OP_ERROR_RSP) { success = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + execute_write_cb)) + return; } else if (opcode != BT_ATT_OP_EXEC_WRITE_RSP || pdu || length) success = false; @@ -3281,6 +3621,10 @@ static void prepare_write_cb(uint8_t opcode, const void *pdu, uint16_t length, if (opcode == BT_ATT_OP_ERROR_RSP) { success = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + prepare_write_cb)) + return; goto done; } @@ -3401,11 +3745,15 @@ unsigned int bt_gatt_client_write_long_value(struct bt_gatt_client *client, put_le16(offset, pdu + 2); memcpy(pdu + 4, op->value, op->cur_length); + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; + req->sent_pdu_len = op->cur_length + 4; + req->sent_pdu = pdu; + req->att_id = bt_att_send(client->att, BT_ATT_OP_PREP_WRITE_REQ, pdu, op->cur_length + 4, prepare_write_cb, req, request_unref); - free(pdu); if (!req->att_id) { op->destroy = NULL; @@ -3450,6 +3798,10 @@ static void prep_write_cb(uint8_t opcode, const void *pdu, uint16_t length, success = false; reliable_error = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + prep_write_cb)) + return; goto done; } @@ -3566,6 +3918,13 @@ unsigned int bt_gatt_client_prepare_write(struct bt_gatt_client *client, memcpy(op->pdu, pdu, length); op->pdu_len = length; + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; + req->sent_pdu_len = length; + req->sent_pdu = malloc(req->sent_pdu_len); + if (req->sent_pdu) + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); + /* * Now we are ready to send command * Note that request_unref will be done on write execute @@ -3600,6 +3959,10 @@ static void exec_write_cb(uint8_t opcode, const void *pdu, uint16_t length, if (opcode == BT_ATT_OP_ERROR_RSP) { success = false; att_ecode = process_error(pdu, length); + if (process_db_out_of_sync(req->client, att_ecode, + pdu, req, + exec_write_cb)) + return; goto done; } @@ -3659,6 +4022,13 @@ unsigned int bt_gatt_client_write_execute(struct bt_gatt_client *client, req->data = op; req->destroy = destroy_write_op; + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ + req->sent_opcode = BT_ATT_OP_EXEC_WRITE_REQ; + req->sent_pdu_len = sizeof(pdu); + req->sent_pdu = malloc(req->sent_pdu_len); + if (req->sent_pdu) + memcpy(req->sent_pdu, &pdu, req->sent_pdu_len); + req->att_id = bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, sizeof(pdu), exec_write_cb, req, request_unref); diff --git a/src/shared/gatt-helpers.c b/src/shared/gatt-helpers.c index c1cbbdc91..e3a6548c4 100644 --- a/src/shared/gatt-helpers.c +++ b/src/shared/gatt-helpers.c @@ -790,6 +790,22 @@ done: discovery_op_complete(op, success, att_ecode); } +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request) +{ + if (!request) + return 0; + + return request->start_handle; +} + +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request) +{ + if (!request) + return 0; + + return request->end_handle; +} + static struct bt_gatt_request *discover_services(struct bt_att *att, bt_uuid_t *uuid, uint16_t start, uint16_t end, diff --git a/src/shared/gatt-helpers.h b/src/shared/gatt-helpers.h index 7623862e9..7a51ec619 100644 --- a/src/shared/gatt-helpers.h +++ b/src/shared/gatt-helpers.h @@ -101,3 +101,6 @@ bool bt_gatt_read_by_type(struct bt_att *att, uint16_t start, uint16_t end, bt_gatt_request_callback_t callback, void *user_data, bt_gatt_destroy_func_t destroy); + +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request); +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request); -- 2.34.1 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* RE: gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching. 2026-01-05 10:38 ` [PATCH v2 1/1] " Mengshi Wu @ 2026-01-05 10:54 ` bluez.test.bot 2026-01-05 19:21 ` [PATCH v2 1/1] " Luiz Augusto von Dentz 1 sibling, 0 replies; 6+ messages in thread From: bluez.test.bot @ 2026-01-05 10:54 UTC (permalink / raw) To: linux-bluetooth, mengshi.wu [-- Attachment #1: Type: text/plain, Size: 6737 bytes --] This is automated email and please do not reply to this email! Dear submitter, Thank you for submitting the patches to the linux bluetooth mailing list. This is a CI test results with your patch series: PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1038430 ---Test result--- Test Summary: CheckPatch PENDING 0.32 seconds GitLint PENDING 0.42 seconds BuildEll PASS 18.16 seconds BluezMake FAIL 15.57 seconds MakeCheck FAIL 27.17 seconds MakeDistcheck PASS 226.21 seconds CheckValgrind FAIL 13.03 seconds CheckSmatch FAIL 16.42 seconds bluezmakeextell FAIL 11.01 seconds IncrementalBuild PENDING 0.36 seconds ScanBuild FAIL 13.37 seconds Details ############################## Test: CheckPatch - PENDING Desc: Run checkpatch.pl script Output: ############################## Test: GitLint - PENDING Desc: Run gitlint Output: ############################## Test: BluezMake - FAIL Desc: Build BlueZ Output: src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7873: src/shared/libshared_mainloop_la-gatt-client.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: MakeCheck - FAIL Desc: Run Bluez Make Check Output: src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7600: src/shared/libshared_glib_la-gatt-client.lo] Error 1 make: *** [Makefile:11022: check] Error 2 ############################## Test: CheckValgrind - FAIL Desc: Run Bluez Make Check with Valgrind Output: src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7873: src/shared/libshared_mainloop_la-gatt-client.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:11022: check] Error 2 ############################## Test: CheckSmatch - FAIL Desc: Run smatch tool with source Output: src/shared/crypto.c:271:21: warning: Variable length array is used. src/shared/crypto.c:272:23: warning: Variable length array is used. src/shared/gatt-helpers.c:768:31: warning: Variable length array is used. src/shared/gatt-helpers.c:846:31: warning: Variable length array is used. src/shared/gatt-helpers.c:1339:31: warning: Variable length array is used. src/shared/gatt-helpers.c:1370:23: warning: Variable length array is used. src/shared/gatt-server.c:278:25: warning: Variable length array is used. src/shared/gatt-server.c:618:25: warning: Variable length array is used. src/shared/gatt-server.c:716:25: warning: Variable length array is used. src/shared/bap.c:312:25: warning: array of flexible structures src/shared/bap.c: note: in included file: ./src/shared/ascs.h:88:25: warning: array of flexible structures src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7873: src/shared/libshared_mainloop_la-gatt-client.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: bluezmakeextell - FAIL Desc: Build Bluez with External ELL Output: src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7873: src/shared/libshared_mainloop_la-gatt-client.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: IncrementalBuild - PENDING Desc: Incremental build with the patches in the series Output: ############################## Test: ScanBuild - FAIL Desc: Run Scan Build Output: src/shared/gatt-client.c: In function ‘db_hash_check_read_cb’: src/shared/gatt-client.c:2028:29: error: unused variable ‘op’ [-Werror=unused-variable] 2028 | struct service_changed_op *op; | ^~ src/shared/gatt-client.c: In function ‘process_db_out_of_sync’: src/shared/gatt-client.c:2172:29: error: unused variable ‘op’ [-Werror=unused-variable] 2172 | struct service_changed_op *op; | ^~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7873: src/shared/libshared_mainloop_la-gatt-client.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 --- Regards, Linux Bluetooth ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2 1/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching. 2026-01-05 10:38 ` [PATCH v2 1/1] " Mengshi Wu 2026-01-05 10:54 ` bluez.test.bot @ 2026-01-05 19:21 ` Luiz Augusto von Dentz 2026-01-06 6:00 ` Mengshi Wu 1 sibling, 1 reply; 6+ messages in thread From: Luiz Augusto von Dentz @ 2026-01-05 19:21 UTC (permalink / raw) To: Mengshi Wu Cc: linux-bluetooth, shuai.zhang, cheng.jiang, chezhou, wei.deng, yiboz Hi Mengshi, On Mon, Jan 5, 2026 at 5:38 AM Mengshi Wu <mengshi.wu@oss.qualcomm.com> wrote: > > Implement automatic recovery when ATT_ECODE_DB_OUT_OF_SYNC error is > received from the remote device. The recovery mechanism: > > - Detects DB_OUT_OF_SYNC errors during GATT operations > - Extracts affected handles from the original request PDU > - Checks if Service Changed indications overlap with those handles > - Verifies database consistency using Database Hash characteristic > - Automatically retries the original request if DB is consistent > - Automatically retries the original request if handle is not affected > > This may prevent some application-level failures when the GATT database > changes on the remote device. Some btmon traffic corresponding to the above would be great to see in the commit description. > > Signed-off-by: Mengshi Wu <mengshi.wu@oss.qualcomm.com> > --- > src/shared/gatt-client.c | 376 +++++++++++++++++++++++++++++++++++++- > src/shared/gatt-helpers.c | 16 ++ > src/shared/gatt-helpers.h | 3 + > 3 files changed, 392 insertions(+), 3 deletions(-) > > diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c > index f8ebab3fa..22b6c5d1d 100644 > --- a/src/shared/gatt-client.c > +++ b/src/shared/gatt-client.c > @@ -41,6 +41,9 @@ > gatt_log(_client, "[%p] %s:%s() " _format, _client, __FILE__, \ > __func__, ## arg) > > +#define DB_OUT_OF_SYNC_HOLD_ON true > +#define DB_OUT_OF_SYNC_GO_ON false This is a bad start, why do you have to use these defines for a boolean? > struct ready_cb { > bt_gatt_client_callback_t callback; > bt_gatt_client_destroy_func_t destroy; > @@ -114,6 +117,9 @@ struct bt_gatt_client { > > struct bt_gatt_request *discovery_req; > unsigned int mtu_req_id; > + > + /* Pending DB out of sync handling */ > + struct db_out_of_sync_data *pending_db_sync; > }; > > struct request { > @@ -126,8 +132,31 @@ struct request { > unsigned int att_id; > void *data; > void (*destroy)(void *); > + > + /* For DB_OUT_OF_SYNC recovery capability */ > + uint8_t *sent_pdu; > + uint16_t sent_pdu_len; > + uint8_t sent_opcode; > +}; > + > +struct db_out_of_sync_data { > + struct bt_gatt_client *client; > + struct request *original_req; > + uint8_t opcode; > + uint8_t *pdu; > + uint16_t pdu_len; > + bt_att_response_func_t att_callback; > + uint16_t *handles; > + uint8_t num_handles; > + bool handle_overlaps; > + bool svc_changed_arrived; > + /* Store original error PDU */ > + struct bt_att_pdu_error_rsp error_pdu; > }; This actually tells me the handling of such functionality shall really be in att.c not gat-client.c, since the former already does the handling of things like BT_ATT_ERROR_AUTHENTICATION so it just reschedules the att_send_op, perhaps we need a callback for the error_rsp to be processed by gatt-client.c as we handle things like gatt db hash, etc, there, but there retry logic we shall be able to use the att.c. > +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data); > +static void call_original_callback_with_error(struct db_out_of_sync_data *data); > + > static struct request *request_ref(struct request *req) > { > __sync_fetch_and_add(&req->ref_count, 1); > @@ -210,6 +239,7 @@ static void request_unref(void *data) > notify_client_idle(client); > } > > + free(req->sent_pdu); > free(req); > } > > @@ -1968,11 +1998,272 @@ fail: > "Failed to initiate service discovery after Service Changed"); > } > > +static void db_out_of_sync_recover(struct bt_gatt_client *client) > +{ > + struct db_out_of_sync_data *pending = client->pending_db_sync; > + unsigned int new_att_id = 0; > + > + assert(pending); > + > + new_att_id = bt_att_send(client->att, pending->opcode, pending->pdu, > + pending->pdu_len, pending->att_callback, > + request_ref(pending->original_req), > + request_unref); > + if (new_att_id) > + pending->original_req->att_id = new_att_id; > + else > + call_original_callback_with_error(pending); > + client->pending_db_sync = NULL; > + db_out_of_sync_data_free(pending); > +} > + > +static void db_hash_check_read_cb(bool success, uint8_t att_ecode, > + struct bt_gatt_result *result, > + void *user_data) > +{ > + struct bt_gatt_client *client = user_data; > + struct db_out_of_sync_data *pending = client->pending_db_sync; > + const uint8_t *local_hash = NULL, *remote_hash; > + struct gatt_db_attribute *hash_attr = NULL; > + struct service_changed_op *op; > + struct bt_gatt_iter iter; > + bt_uuid_t uuid; > + uint16_t handle, len; > + > + assert(pending); > + > + if (pending->svc_changed_arrived) { > + if (!pending->handle_overlaps) { > + /* No overlap - resend original request */ > + DBG(client, "Service changed range doesn't overlap"); > + db_out_of_sync_recover(client); > + } > + > + return; > + } > + > + /* If read failed, fall back to full re-discovery */ > + if (!success) > + goto trigger_rediscovery; > + > + if (!result || !bt_gatt_iter_init(&iter, result)) > + goto trigger_rediscovery; > + > + if (!bt_gatt_iter_next_read_by_type(&iter, &handle, > + &len, &remote_hash)) > + goto trigger_rediscovery; > + > + if (len != 16) > + goto trigger_rediscovery; > + > + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); > + gatt_db_find_by_type(client->db, 0x0001, 0xffff, &uuid, > + get_first_attribute, &hash_attr); > + > + if (hash_attr) { > + gatt_db_attribute_read(hash_attr, 0, BT_ATT_OP_READ_REQ, NULL, > + db_hash_read_value_cb, &local_hash); > + } > + > + /* If hashes match, no need to trigger re-discovery */ > + if (local_hash && !memcmp(local_hash, remote_hash, 16)) { > + db_out_of_sync_recover(client); > + return; > + } > + > + DBG(client, "DB Hash mismatch: triggering re-discovery"); > + > +trigger_rediscovery: > + call_original_callback_with_error(pending); > + client->pending_db_sync = NULL; > + db_out_of_sync_data_free(pending); > + > + process_service_changed(client, 0x0001, 0xffff); > +} > + > +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data) > +{ > + if (!data) > + return; > + > + if (data->original_req) > + request_unref(data->original_req); > + > + free(data->pdu); > + free(data->handles); > + free(data); > +} > + > +static bool check_handle_overlap(uint16_t *handles, uint8_t num_handles, > + uint16_t start, uint16_t end) > +{ > + uint8_t i; > + > + if (!handles) > + return true; > + > + for (i = 0; i < num_handles; i++) { > + if (handles[i] >= start && handles[i] <= end) > + return true; > + } > + > + return false; > +} > + > +static uint8_t extract_handles_from_pdu(uint8_t opcode, const uint8_t *pdu, > + uint16_t pdu_len, uint16_t **handles) > +{ > + uint8_t num_handles = 0; > + uint16_t *handle_array; > + uint16_t i; > + > + switch (opcode) { > + case BT_ATT_OP_READ_REQ: > + case BT_ATT_OP_READ_BLOB_REQ: > + case BT_ATT_OP_WRITE_REQ: > + case BT_ATT_OP_WRITE_CMD: > + case BT_ATT_OP_PREP_WRITE_REQ: > + /* Single handle at offset 0 */ > + num_handles = 1; > + handle_array = malloc(sizeof(uint16_t)); > + if (handle_array) > + handle_array[0] = get_le16(pdu); > + break; > + > + case BT_ATT_OP_READ_MULT_REQ: > + case BT_ATT_OP_READ_MULT_VL_REQ: > + /* Multiple handles, 2 bytes each */ > + num_handles = pdu_len / 2; > + handle_array = malloc(num_handles * sizeof(uint16_t)); > + if (handle_array) { > + for (i = 0; i < num_handles; i++) > + handle_array[i] = get_le16(pdu + (i * 2)); > + } > + break; > + > + default: > + return 0; > + } > + > + if (!handle_array) > + return 0; > + > + *handles = handle_array; > + return num_handles; > +} > + > +static void call_original_callback_with_error(struct db_out_of_sync_data *data) > +{ > + struct request *req = data->original_req; > + > + if (!req || !data->att_callback) > + return; > + > + data->att_callback(BT_ATT_OP_ERROR_RSP, &(data->error_pdu), > + sizeof(struct bt_att_pdu_error_rsp), req); > +} > + > +static bool process_db_out_of_sync(struct bt_gatt_client *client, > + uint8_t att_ecode, const void *error_pdu, > + struct request *req, > + bt_att_response_func_t callback) > +{ > + struct db_out_of_sync_data *pending; > + struct service_changed_op *op; > + struct bt_gatt_request *gatt_req_op = client->discovery_req; > + const struct bt_att_pdu_error_rsp *epdu = error_pdu; > + bt_uuid_t uuid; > + unsigned int new_att_id = 0; > + > + if (att_ecode != BT_ATT_ERROR_DB_OUT_OF_SYNC) > + return DB_OUT_OF_SYNC_GO_ON; > + > + /* Check if we already have a pending operation */ > + if (client->pending_db_sync) > + return DB_OUT_OF_SYNC_GO_ON; > + > + /* Only handle if we have the necessary request info */ > + if (!req || !req->sent_pdu || !callback) > + goto trigger_rediscovery; > + > + /* Create pending structure */ > + pending = new0(struct db_out_of_sync_data, 1); > + if (!pending) > + goto trigger_rediscovery; > + > + pending->client = client; > + pending->original_req = request_ref(req); > + pending->att_callback = callback; > + pending->opcode = req->sent_opcode; > + pending->pdu_len = req->sent_pdu_len; > + > + /* Copy PDU */ > + pending->pdu = malloc(pending->pdu_len); > + if (!pending->pdu) { > + db_out_of_sync_data_free(pending); > + goto trigger_rediscovery; > + } > + memcpy(pending->pdu, req->sent_pdu, pending->pdu_len); > + > + /* Store original error PDU */ > + memcpy(&(pending->error_pdu), error_pdu, > + sizeof(struct bt_att_pdu_error_rsp)); > + > + /* Extract handles from PDU */ > + pending->num_handles = > + extract_handles_from_pdu(pending->opcode, pending->pdu, > + pending->pdu_len, &pending->handles); This doesn't sound right, the error response bt_att_pdu_error_rsp already contains the affected handle, why not use it instead of trying to extract from the request? > + if (!pending->num_handles) { > + db_out_of_sync_data_free(pending); > + goto trigger_rediscovery; > + } > + > + /* Store pending operation */ > + client->pending_db_sync = pending; > + > + /* Initiate hash read */ > + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); > + > + if (bt_gatt_read_by_type(client->att, 0x0001, 0xffff, &uuid, > + db_hash_check_read_cb, client, NULL)) { > + return DB_OUT_OF_SYNC_HOLD_ON; > + } > + > + client->pending_db_sync = NULL; > + db_out_of_sync_data_free(pending); > + > +trigger_rediscovery: > + > + if (client->in_svc_chngd) { > + if (client->discovery_req && req && req->sent_pdu && callback && > + (epdu->handle != 0) && gatt_req_op && > + (bt_gatt_request_get_start_handle(gatt_req_op) > > + epdu->handle || > + bt_gatt_request_get_end_handle(gatt_req_op) < > + epdu->handle)) { > + new_att_id = bt_att_send(client->att, req->sent_opcode, > + req->sent_pdu, > + req->sent_pdu_len, callback, > + request_ref(req), > + request_unref); > + if (new_att_id) { > + req->att_id = new_att_id; > + return DB_OUT_OF_SYNC_HOLD_ON; > + } > + } > + return DB_OUT_OF_SYNC_GO_ON; > + } > + > + process_service_changed(client, 0x0001, 0xffff); > + return DB_OUT_OF_SYNC_GO_ON; > +} > + > static void service_changed_cb(uint16_t value_handle, const uint8_t *value, > uint16_t length, void *user_data) > { > struct bt_gatt_client *client = user_data; > struct service_changed_op *op; > + struct db_out_of_sync_data *pending; > uint16_t start, end; > > if (length != 4) > @@ -1990,6 +2281,14 @@ static void service_changed_cb(uint16_t value_handle, const uint8_t *value, > DBG(client, "Service Changed received - start: 0x%04x end: 0x%04x", > start, end); > > + /* Check if there's a pending DB out of sync operation */ > + pending = client->pending_db_sync; > + if (pending) { > + pending->svc_changed_arrived = true; > + pending->handle_overlaps = check_handle_overlap(pending->handles, > + pending->num_handles, start, end); > + } > + > if (!client->in_svc_chngd) { > process_service_changed(client, start, end); > return; > @@ -2332,6 +2631,10 @@ static void att_disconnect_cb(int err, void *user_data) > > client->disc_id = 0; > > + /* Cleanup pending DB out of sync operation */ > + db_out_of_sync_data_free(client->pending_db_sync); > + client->pending_db_sync = NULL; > + > bt_att_unref(client->att); > client->att = NULL; > > @@ -2712,10 +3015,15 @@ static void read_multiple_cb(uint8_t opcode, const void *pdu, uint16_t length, > (!pdu && length)) { > success = false; > > - if (opcode == BT_ATT_OP_ERROR_RSP) > + if (opcode == BT_ATT_OP_ERROR_RSP) { > att_ecode = process_error(pdu, length); > - else > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + read_multiple_cb)) > + return; > + } else { > att_ecode = 0; > + } > > pdu = NULL; > length = 0; > @@ -2799,6 +3107,13 @@ unsigned int bt_gatt_client_read_multiple(struct bt_gatt_client *client, > BT_GATT_CHRC_CLI_FEAT_EATT ? BT_ATT_OP_READ_MULT_VL_REQ : > BT_ATT_OP_READ_MULT_REQ; > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = opcode; > + req->sent_pdu_len = num_handles * 2; > + req->sent_pdu = malloc(req->sent_pdu_len); > + if (req->sent_pdu) > + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); > + > req->att_id = bt_att_send(client->att, opcode, pdu, num_handles * 2, > read_multiple_cb, req, > request_unref); > @@ -2867,6 +3182,10 @@ static void read_long_cb(uint8_t opcode, const void *pdu, > if (opcode == BT_ATT_OP_ERROR_RSP) { > success = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + read_long_cb)) > + return; > goto done; > } > > @@ -2975,6 +3294,13 @@ unsigned int bt_gatt_client_read_long_value(struct bt_gatt_client *client, > att_op = BT_ATT_OP_READ_REQ; > } > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = att_op; > + req->sent_pdu_len = pdu_len; > + req->sent_pdu = malloc(req->sent_pdu_len); > + if (req->sent_pdu) > + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); > + > req->att_id = bt_att_send(client->att, att_op, pdu, pdu_len, > read_long_cb, req, request_unref); > > @@ -3053,6 +3379,9 @@ static void write_cb(uint8_t opcode, const void *pdu, uint16_t length, > if (opcode == BT_ATT_OP_ERROR_RSP) { > success = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, write_cb)) > + return; > goto done; > } > > @@ -3096,6 +3425,13 @@ unsigned int bt_gatt_client_write_value(struct bt_gatt_client *client, > put_le16(value_handle, pdu); > memcpy(pdu + 2, value, length); > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = BT_ATT_OP_WRITE_REQ; > + req->sent_pdu_len = 2 + length; > + req->sent_pdu = malloc(req->sent_pdu_len); > + if (req->sent_pdu) > + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); > + > req->att_id = bt_att_send(client->att, BT_ATT_OP_WRITE_REQ, > pdu, 2 + length, > write_cb, req, > @@ -3216,6 +3552,10 @@ static void execute_write_cb(uint8_t opcode, const void *pdu, uint16_t length, > if (opcode == BT_ATT_OP_ERROR_RSP) { > success = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + execute_write_cb)) > + return; > } else if (opcode != BT_ATT_OP_EXEC_WRITE_RSP || pdu || length) > success = false; > > @@ -3281,6 +3621,10 @@ static void prepare_write_cb(uint8_t opcode, const void *pdu, uint16_t length, > if (opcode == BT_ATT_OP_ERROR_RSP) { > success = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + prepare_write_cb)) > + return; > goto done; > } > > @@ -3401,11 +3745,15 @@ unsigned int bt_gatt_client_write_long_value(struct bt_gatt_client *client, > put_le16(offset, pdu + 2); > memcpy(pdu + 4, op->value, op->cur_length); > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; > + req->sent_pdu_len = op->cur_length + 4; > + req->sent_pdu = pdu; > + > req->att_id = bt_att_send(client->att, BT_ATT_OP_PREP_WRITE_REQ, > pdu, op->cur_length + 4, > prepare_write_cb, req, > request_unref); > - free(pdu); > > if (!req->att_id) { > op->destroy = NULL; > @@ -3450,6 +3798,10 @@ static void prep_write_cb(uint8_t opcode, const void *pdu, uint16_t length, > success = false; > reliable_error = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + prep_write_cb)) > + return; > goto done; > } > > @@ -3566,6 +3918,13 @@ unsigned int bt_gatt_client_prepare_write(struct bt_gatt_client *client, > memcpy(op->pdu, pdu, length); > op->pdu_len = length; > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; > + req->sent_pdu_len = length; > + req->sent_pdu = malloc(req->sent_pdu_len); > + if (req->sent_pdu) > + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); > + > /* > * Now we are ready to send command > * Note that request_unref will be done on write execute > @@ -3600,6 +3959,10 @@ static void exec_write_cb(uint8_t opcode, const void *pdu, uint16_t length, > if (opcode == BT_ATT_OP_ERROR_RSP) { > success = false; > att_ecode = process_error(pdu, length); > + if (process_db_out_of_sync(req->client, att_ecode, > + pdu, req, > + exec_write_cb)) > + return; > goto done; > } > > @@ -3659,6 +4022,13 @@ unsigned int bt_gatt_client_write_execute(struct bt_gatt_client *client, > req->data = op; > req->destroy = destroy_write_op; > > + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ > + req->sent_opcode = BT_ATT_OP_EXEC_WRITE_REQ; > + req->sent_pdu_len = sizeof(pdu); > + req->sent_pdu = malloc(req->sent_pdu_len); > + if (req->sent_pdu) > + memcpy(req->sent_pdu, &pdu, req->sent_pdu_len); This is sort of repeated in all requests thus why I think we are better off doing it in att.c, so we don't have to keep calling process_db_out_of_sync and then copy the requests which is actually a duplicating what att.c is doing with att_send_op. > req->att_id = bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, > sizeof(pdu), exec_write_cb, > req, request_unref); > diff --git a/src/shared/gatt-helpers.c b/src/shared/gatt-helpers.c > index c1cbbdc91..e3a6548c4 100644 > --- a/src/shared/gatt-helpers.c > +++ b/src/shared/gatt-helpers.c > @@ -790,6 +790,22 @@ done: > discovery_op_complete(op, success, att_ecode); > } > > +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request) > +{ > + if (!request) > + return 0; > + > + return request->start_handle; > +} > + > +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request) > +{ > + if (!request) > + return 0; > + > + return request->end_handle; > +} > + > static struct bt_gatt_request *discover_services(struct bt_att *att, > bt_uuid_t *uuid, > uint16_t start, uint16_t end, > diff --git a/src/shared/gatt-helpers.h b/src/shared/gatt-helpers.h > index 7623862e9..7a51ec619 100644 > --- a/src/shared/gatt-helpers.h > +++ b/src/shared/gatt-helpers.h > @@ -101,3 +101,6 @@ bool bt_gatt_read_by_type(struct bt_att *att, uint16_t start, uint16_t end, > bt_gatt_request_callback_t callback, > void *user_data, > bt_gatt_destroy_func_t destroy); > + > +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request); > +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request); > -- > 2.34.1 > -- Luiz Augusto von Dentz ^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH v2 1/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching. 2026-01-05 19:21 ` [PATCH v2 1/1] " Luiz Augusto von Dentz @ 2026-01-06 6:00 ` Mengshi Wu 0 siblings, 0 replies; 6+ messages in thread From: Mengshi Wu @ 2026-01-06 6:00 UTC (permalink / raw) To: Luiz Augusto von Dentz Cc: linux-bluetooth, shuai.zhang, cheng.jiang, chezhou, wei.deng, yiboz Hi Luiz, Thanks for review and comments. On 1/6/2026 3:21 AM, Luiz Augusto von Dentz wrote: > Hi Mengshi, > > On Mon, Jan 5, 2026 at 5:38 AM Mengshi Wu <mengshi.wu@oss.qualcomm.com> wrote: >> >> Implement automatic recovery when ATT_ECODE_DB_OUT_OF_SYNC error is >> received from the remote device. The recovery mechanism: >> >> - Detects DB_OUT_OF_SYNC errors during GATT operations >> - Extracts affected handles from the original request PDU >> - Checks if Service Changed indications overlap with those handles >> - Verifies database consistency using Database Hash characteristic >> - Automatically retries the original request if DB is consistent >> - Automatically retries the original request if handle is not affected >> >> This may prevent some application-level failures when the GATT database >> changes on the remote device. > > Some btmon traffic corresponding to the above would be great to see in > the commit description. OK, I will add some traffic traces in the next commit description. > >> >> Signed-off-by: Mengshi Wu <mengshi.wu@oss.qualcomm.com> >> --- >> src/shared/gatt-client.c | 376 +++++++++++++++++++++++++++++++++++++- >> src/shared/gatt-helpers.c | 16 ++ >> src/shared/gatt-helpers.h | 3 + >> 3 files changed, 392 insertions(+), 3 deletions(-) >> >> diff --git a/src/shared/gatt-client.c b/src/shared/gatt-client.c >> index f8ebab3fa..22b6c5d1d 100644 >> --- a/src/shared/gatt-client.c >> +++ b/src/shared/gatt-client.c >> @@ -41,6 +41,9 @@ >> gatt_log(_client, "[%p] %s:%s() " _format, _client, __FILE__, \ >> __func__, ## arg) >> >> +#define DB_OUT_OF_SYNC_HOLD_ON true >> +#define DB_OUT_OF_SYNC_GO_ON false > > This is a bad start, why do you have to use these defines for a boolean? I added these macros for process_db_out_of_sync() to improve readability. I think this may make the code clearer than simply returning a boolean value. If this is not considered appropriate usage, I will remove them. > >> struct ready_cb { >> bt_gatt_client_callback_t callback; >> bt_gatt_client_destroy_func_t destroy; >> @@ -114,6 +117,9 @@ struct bt_gatt_client { >> >> struct bt_gatt_request *discovery_req; >> unsigned int mtu_req_id; >> + >> + /* Pending DB out of sync handling */ >> + struct db_out_of_sync_data *pending_db_sync; >> }; >> >> struct request { >> @@ -126,8 +132,31 @@ struct request { >> unsigned int att_id; >> void *data; >> void (*destroy)(void *); >> + >> + /* For DB_OUT_OF_SYNC recovery capability */ >> + uint8_t *sent_pdu; >> + uint16_t sent_pdu_len; >> + uint8_t sent_opcode; >> +}; >> + >> +struct db_out_of_sync_data { >> + struct bt_gatt_client *client; >> + struct request *original_req; >> + uint8_t opcode; >> + uint8_t *pdu; >> + uint16_t pdu_len; >> + bt_att_response_func_t att_callback; >> + uint16_t *handles; >> + uint8_t num_handles; >> + bool handle_overlaps; >> + bool svc_changed_arrived; >> + /* Store original error PDU */ >> + struct bt_att_pdu_error_rsp error_pdu; >> }; > > This actually tells me the handling of such functionality shall really > be in att.c not gat-client.c, since the former already does the > handling of things like BT_ATT_ERROR_AUTHENTICATION so it just > reschedules the att_send_op, perhaps we need a callback for the > error_rsp to be processed by gatt-client.c as we handle things like > gatt db hash, etc, there, but there retry logic we shall be able to > use the att.c. OK, I noticed that att.c already contains some resend logic. I will try moving the retry logic from gatt-client.c to att.c to avoid storing PDU data in both sides and to reuse the existing implementation. > >> +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data); >> +static void call_original_callback_with_error(struct db_out_of_sync_data *data); >> + >> static struct request *request_ref(struct request *req) >> { >> __sync_fetch_and_add(&req->ref_count, 1); >> @@ -210,6 +239,7 @@ static void request_unref(void *data) >> notify_client_idle(client); >> } >> >> + free(req->sent_pdu); >> free(req); >> } >> >> @@ -1968,11 +1998,272 @@ fail: >> "Failed to initiate service discovery after Service Changed"); >> } >> >> +static void db_out_of_sync_recover(struct bt_gatt_client *client) >> +{ >> + struct db_out_of_sync_data *pending = client->pending_db_sync; >> + unsigned int new_att_id = 0; >> + >> + assert(pending); >> + >> + new_att_id = bt_att_send(client->att, pending->opcode, pending->pdu, >> + pending->pdu_len, pending->att_callback, >> + request_ref(pending->original_req), >> + request_unref); >> + if (new_att_id) >> + pending->original_req->att_id = new_att_id; >> + else >> + call_original_callback_with_error(pending); >> + client->pending_db_sync = NULL; >> + db_out_of_sync_data_free(pending); >> +} >> + >> +static void db_hash_check_read_cb(bool success, uint8_t att_ecode, >> + struct bt_gatt_result *result, >> + void *user_data) >> +{ >> + struct bt_gatt_client *client = user_data; >> + struct db_out_of_sync_data *pending = client->pending_db_sync; >> + const uint8_t *local_hash = NULL, *remote_hash; >> + struct gatt_db_attribute *hash_attr = NULL; >> + struct service_changed_op *op; >> + struct bt_gatt_iter iter; >> + bt_uuid_t uuid; >> + uint16_t handle, len; >> + >> + assert(pending); >> + >> + if (pending->svc_changed_arrived) { >> + if (!pending->handle_overlaps) { >> + /* No overlap - resend original request */ >> + DBG(client, "Service changed range doesn't overlap"); >> + db_out_of_sync_recover(client); >> + } >> + >> + return; >> + } >> + >> + /* If read failed, fall back to full re-discovery */ >> + if (!success) >> + goto trigger_rediscovery; >> + >> + if (!result || !bt_gatt_iter_init(&iter, result)) >> + goto trigger_rediscovery; >> + >> + if (!bt_gatt_iter_next_read_by_type(&iter, &handle, >> + &len, &remote_hash)) >> + goto trigger_rediscovery; >> + >> + if (len != 16) >> + goto trigger_rediscovery; >> + >> + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); >> + gatt_db_find_by_type(client->db, 0x0001, 0xffff, &uuid, >> + get_first_attribute, &hash_attr); >> + >> + if (hash_attr) { >> + gatt_db_attribute_read(hash_attr, 0, BT_ATT_OP_READ_REQ, NULL, >> + db_hash_read_value_cb, &local_hash); >> + } >> + >> + /* If hashes match, no need to trigger re-discovery */ >> + if (local_hash && !memcmp(local_hash, remote_hash, 16)) { >> + db_out_of_sync_recover(client); >> + return; >> + } >> + >> + DBG(client, "DB Hash mismatch: triggering re-discovery"); >> + >> +trigger_rediscovery: >> + call_original_callback_with_error(pending); >> + client->pending_db_sync = NULL; >> + db_out_of_sync_data_free(pending); >> + >> + process_service_changed(client, 0x0001, 0xffff); >> +} >> + >> +static void db_out_of_sync_data_free(struct db_out_of_sync_data *data) >> +{ >> + if (!data) >> + return; >> + >> + if (data->original_req) >> + request_unref(data->original_req); >> + >> + free(data->pdu); >> + free(data->handles); >> + free(data); >> +} >> + >> +static bool check_handle_overlap(uint16_t *handles, uint8_t num_handles, >> + uint16_t start, uint16_t end) >> +{ >> + uint8_t i; >> + >> + if (!handles) >> + return true; >> + >> + for (i = 0; i < num_handles; i++) { >> + if (handles[i] >= start && handles[i] <= end) >> + return true; >> + } >> + >> + return false; >> +} >> + >> +static uint8_t extract_handles_from_pdu(uint8_t opcode, const uint8_t *pdu, >> + uint16_t pdu_len, uint16_t **handles) >> +{ >> + uint8_t num_handles = 0; >> + uint16_t *handle_array; >> + uint16_t i; >> + >> + switch (opcode) { >> + case BT_ATT_OP_READ_REQ: >> + case BT_ATT_OP_READ_BLOB_REQ: >> + case BT_ATT_OP_WRITE_REQ: >> + case BT_ATT_OP_WRITE_CMD: >> + case BT_ATT_OP_PREP_WRITE_REQ: >> + /* Single handle at offset 0 */ >> + num_handles = 1; >> + handle_array = malloc(sizeof(uint16_t)); >> + if (handle_array) >> + handle_array[0] = get_le16(pdu); >> + break; >> + >> + case BT_ATT_OP_READ_MULT_REQ: >> + case BT_ATT_OP_READ_MULT_VL_REQ: >> + /* Multiple handles, 2 bytes each */ >> + num_handles = pdu_len / 2; >> + handle_array = malloc(num_handles * sizeof(uint16_t)); >> + if (handle_array) { >> + for (i = 0; i < num_handles; i++) >> + handle_array[i] = get_le16(pdu + (i * 2)); >> + } >> + break; >> + >> + default: >> + return 0; >> + } >> + >> + if (!handle_array) >> + return 0; >> + >> + *handles = handle_array; >> + return num_handles; >> +} >> + >> +static void call_original_callback_with_error(struct db_out_of_sync_data *data) >> +{ >> + struct request *req = data->original_req; >> + >> + if (!req || !data->att_callback) >> + return; >> + >> + data->att_callback(BT_ATT_OP_ERROR_RSP, &(data->error_pdu), >> + sizeof(struct bt_att_pdu_error_rsp), req); >> +} >> + >> +static bool process_db_out_of_sync(struct bt_gatt_client *client, >> + uint8_t att_ecode, const void *error_pdu, >> + struct request *req, >> + bt_att_response_func_t callback) >> +{ >> + struct db_out_of_sync_data *pending; >> + struct service_changed_op *op; >> + struct bt_gatt_request *gatt_req_op = client->discovery_req; >> + const struct bt_att_pdu_error_rsp *epdu = error_pdu; >> + bt_uuid_t uuid; >> + unsigned int new_att_id = 0; >> + >> + if (att_ecode != BT_ATT_ERROR_DB_OUT_OF_SYNC) >> + return DB_OUT_OF_SYNC_GO_ON; >> + >> + /* Check if we already have a pending operation */ >> + if (client->pending_db_sync) >> + return DB_OUT_OF_SYNC_GO_ON; >> + >> + /* Only handle if we have the necessary request info */ >> + if (!req || !req->sent_pdu || !callback) >> + goto trigger_rediscovery; >> + >> + /* Create pending structure */ >> + pending = new0(struct db_out_of_sync_data, 1); >> + if (!pending) >> + goto trigger_rediscovery; >> + >> + pending->client = client; >> + pending->original_req = request_ref(req); >> + pending->att_callback = callback; >> + pending->opcode = req->sent_opcode; >> + pending->pdu_len = req->sent_pdu_len; >> + >> + /* Copy PDU */ >> + pending->pdu = malloc(pending->pdu_len); >> + if (!pending->pdu) { >> + db_out_of_sync_data_free(pending); >> + goto trigger_rediscovery; >> + } >> + memcpy(pending->pdu, req->sent_pdu, pending->pdu_len); >> + >> + /* Store original error PDU */ >> + memcpy(&(pending->error_pdu), error_pdu, >> + sizeof(struct bt_att_pdu_error_rsp)); >> + >> + /* Extract handles from PDU */ >> + pending->num_handles = >> + extract_handles_from_pdu(pending->opcode, pending->pdu, >> + pending->pdu_len, &pending->handles); > > This doesn't sound right, the error response bt_att_pdu_error_rsp > already contains the affected handle, why not use it instead of trying > to extract from the request? I will remove this and use handle in the bt_att_pdu_error_rsp directly. > >> + if (!pending->num_handles) { >> + db_out_of_sync_data_free(pending); >> + goto trigger_rediscovery; >> + } >> + >> + /* Store pending operation */ >> + client->pending_db_sync = pending; >> + >> + /* Initiate hash read */ >> + bt_uuid16_create(&uuid, GATT_CHARAC_DB_HASH); >> + >> + if (bt_gatt_read_by_type(client->att, 0x0001, 0xffff, &uuid, >> + db_hash_check_read_cb, client, NULL)) { >> + return DB_OUT_OF_SYNC_HOLD_ON; >> + } >> + >> + client->pending_db_sync = NULL; >> + db_out_of_sync_data_free(pending); >> + >> +trigger_rediscovery: >> + >> + if (client->in_svc_chngd) { >> + if (client->discovery_req && req && req->sent_pdu && callback && >> + (epdu->handle != 0) && gatt_req_op && >> + (bt_gatt_request_get_start_handle(gatt_req_op) > >> + epdu->handle || >> + bt_gatt_request_get_end_handle(gatt_req_op) < >> + epdu->handle)) { >> + new_att_id = bt_att_send(client->att, req->sent_opcode, >> + req->sent_pdu, >> + req->sent_pdu_len, callback, >> + request_ref(req), >> + request_unref); >> + if (new_att_id) { >> + req->att_id = new_att_id; >> + return DB_OUT_OF_SYNC_HOLD_ON; >> + } >> + } >> + return DB_OUT_OF_SYNC_GO_ON; >> + } >> + >> + process_service_changed(client, 0x0001, 0xffff); >> + return DB_OUT_OF_SYNC_GO_ON; >> +} >> + >> static void service_changed_cb(uint16_t value_handle, const uint8_t *value, >> uint16_t length, void *user_data) >> { >> struct bt_gatt_client *client = user_data; >> struct service_changed_op *op; >> + struct db_out_of_sync_data *pending; >> uint16_t start, end; >> >> if (length != 4) >> @@ -1990,6 +2281,14 @@ static void service_changed_cb(uint16_t value_handle, const uint8_t *value, >> DBG(client, "Service Changed received - start: 0x%04x end: 0x%04x", >> start, end); >> >> + /* Check if there's a pending DB out of sync operation */ >> + pending = client->pending_db_sync; >> + if (pending) { >> + pending->svc_changed_arrived = true; >> + pending->handle_overlaps = check_handle_overlap(pending->handles, >> + pending->num_handles, start, end); >> + } >> + >> if (!client->in_svc_chngd) { >> process_service_changed(client, start, end); >> return; >> @@ -2332,6 +2631,10 @@ static void att_disconnect_cb(int err, void *user_data) >> >> client->disc_id = 0; >> >> + /* Cleanup pending DB out of sync operation */ >> + db_out_of_sync_data_free(client->pending_db_sync); >> + client->pending_db_sync = NULL; >> + >> bt_att_unref(client->att); >> client->att = NULL; >> >> @@ -2712,10 +3015,15 @@ static void read_multiple_cb(uint8_t opcode, const void *pdu, uint16_t length, >> (!pdu && length)) { >> success = false; >> >> - if (opcode == BT_ATT_OP_ERROR_RSP) >> + if (opcode == BT_ATT_OP_ERROR_RSP) { >> att_ecode = process_error(pdu, length); >> - else >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + read_multiple_cb)) >> + return; >> + } else { >> att_ecode = 0; >> + } >> >> pdu = NULL; >> length = 0; >> @@ -2799,6 +3107,13 @@ unsigned int bt_gatt_client_read_multiple(struct bt_gatt_client *client, >> BT_GATT_CHRC_CLI_FEAT_EATT ? BT_ATT_OP_READ_MULT_VL_REQ : >> BT_ATT_OP_READ_MULT_REQ; >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = opcode; >> + req->sent_pdu_len = num_handles * 2; >> + req->sent_pdu = malloc(req->sent_pdu_len); >> + if (req->sent_pdu) >> + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); >> + >> req->att_id = bt_att_send(client->att, opcode, pdu, num_handles * 2, >> read_multiple_cb, req, >> request_unref); >> @@ -2867,6 +3182,10 @@ static void read_long_cb(uint8_t opcode, const void *pdu, >> if (opcode == BT_ATT_OP_ERROR_RSP) { >> success = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + read_long_cb)) >> + return; >> goto done; >> } >> >> @@ -2975,6 +3294,13 @@ unsigned int bt_gatt_client_read_long_value(struct bt_gatt_client *client, >> att_op = BT_ATT_OP_READ_REQ; >> } >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = att_op; >> + req->sent_pdu_len = pdu_len; >> + req->sent_pdu = malloc(req->sent_pdu_len); >> + if (req->sent_pdu) >> + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); >> + >> req->att_id = bt_att_send(client->att, att_op, pdu, pdu_len, >> read_long_cb, req, request_unref); >> >> @@ -3053,6 +3379,9 @@ static void write_cb(uint8_t opcode, const void *pdu, uint16_t length, >> if (opcode == BT_ATT_OP_ERROR_RSP) { >> success = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, write_cb)) >> + return; >> goto done; >> } >> >> @@ -3096,6 +3425,13 @@ unsigned int bt_gatt_client_write_value(struct bt_gatt_client *client, >> put_le16(value_handle, pdu); >> memcpy(pdu + 2, value, length); >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = BT_ATT_OP_WRITE_REQ; >> + req->sent_pdu_len = 2 + length; >> + req->sent_pdu = malloc(req->sent_pdu_len); >> + if (req->sent_pdu) >> + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); >> + >> req->att_id = bt_att_send(client->att, BT_ATT_OP_WRITE_REQ, >> pdu, 2 + length, >> write_cb, req, >> @@ -3216,6 +3552,10 @@ static void execute_write_cb(uint8_t opcode, const void *pdu, uint16_t length, >> if (opcode == BT_ATT_OP_ERROR_RSP) { >> success = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + execute_write_cb)) >> + return; >> } else if (opcode != BT_ATT_OP_EXEC_WRITE_RSP || pdu || length) >> success = false; >> >> @@ -3281,6 +3621,10 @@ static void prepare_write_cb(uint8_t opcode, const void *pdu, uint16_t length, >> if (opcode == BT_ATT_OP_ERROR_RSP) { >> success = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + prepare_write_cb)) >> + return; >> goto done; >> } >> >> @@ -3401,11 +3745,15 @@ unsigned int bt_gatt_client_write_long_value(struct bt_gatt_client *client, >> put_le16(offset, pdu + 2); >> memcpy(pdu + 4, op->value, op->cur_length); >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; >> + req->sent_pdu_len = op->cur_length + 4; >> + req->sent_pdu = pdu; >> + >> req->att_id = bt_att_send(client->att, BT_ATT_OP_PREP_WRITE_REQ, >> pdu, op->cur_length + 4, >> prepare_write_cb, req, >> request_unref); >> - free(pdu); >> >> if (!req->att_id) { >> op->destroy = NULL; >> @@ -3450,6 +3798,10 @@ static void prep_write_cb(uint8_t opcode, const void *pdu, uint16_t length, >> success = false; >> reliable_error = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + prep_write_cb)) >> + return; >> goto done; >> } >> >> @@ -3566,6 +3918,13 @@ unsigned int bt_gatt_client_prepare_write(struct bt_gatt_client *client, >> memcpy(op->pdu, pdu, length); >> op->pdu_len = length; >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = BT_ATT_OP_PREP_WRITE_REQ; >> + req->sent_pdu_len = length; >> + req->sent_pdu = malloc(req->sent_pdu_len); >> + if (req->sent_pdu) >> + memcpy(req->sent_pdu, pdu, req->sent_pdu_len); >> + >> /* >> * Now we are ready to send command >> * Note that request_unref will be done on write execute >> @@ -3600,6 +3959,10 @@ static void exec_write_cb(uint8_t opcode, const void *pdu, uint16_t length, >> if (opcode == BT_ATT_OP_ERROR_RSP) { >> success = false; >> att_ecode = process_error(pdu, length); >> + if (process_db_out_of_sync(req->client, att_ecode, >> + pdu, req, >> + exec_write_cb)) >> + return; >> goto done; >> } >> >> @@ -3659,6 +4022,13 @@ unsigned int bt_gatt_client_write_execute(struct bt_gatt_client *client, >> req->data = op; >> req->destroy = destroy_write_op; >> >> + /* Store PDU for potential resend on DB_OUT_OF_SYNC */ >> + req->sent_opcode = BT_ATT_OP_EXEC_WRITE_REQ; >> + req->sent_pdu_len = sizeof(pdu); >> + req->sent_pdu = malloc(req->sent_pdu_len); >> + if (req->sent_pdu) >> + memcpy(req->sent_pdu, &pdu, req->sent_pdu_len); > > > This is sort of repeated in all requests thus why I think we are > better off doing it in att.c, so we don't have to keep calling > process_db_out_of_sync and then copy the requests which is actually a > duplicating what att.c is doing with att_send_op. Yes, I see that att.c has already stored requests. I will move this logic to att.c. > >> req->att_id = bt_att_send(client->att, BT_ATT_OP_EXEC_WRITE_REQ, &pdu, >> sizeof(pdu), exec_write_cb, >> req, request_unref); >> diff --git a/src/shared/gatt-helpers.c b/src/shared/gatt-helpers.c >> index c1cbbdc91..e3a6548c4 100644 >> --- a/src/shared/gatt-helpers.c >> +++ b/src/shared/gatt-helpers.c >> @@ -790,6 +790,22 @@ done: >> discovery_op_complete(op, success, att_ecode); >> } >> >> +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request) >> +{ >> + if (!request) >> + return 0; >> + >> + return request->start_handle; >> +} >> + >> +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request) >> +{ >> + if (!request) >> + return 0; >> + >> + return request->end_handle; >> +} >> + >> static struct bt_gatt_request *discover_services(struct bt_att *att, >> bt_uuid_t *uuid, >> uint16_t start, uint16_t end, >> diff --git a/src/shared/gatt-helpers.h b/src/shared/gatt-helpers.h >> index 7623862e9..7a51ec619 100644 >> --- a/src/shared/gatt-helpers.h >> +++ b/src/shared/gatt-helpers.h >> @@ -101,3 +101,6 @@ bool bt_gatt_read_by_type(struct bt_att *att, uint16_t start, uint16_t end, >> bt_gatt_request_callback_t callback, >> void *user_data, >> bt_gatt_destroy_func_t destroy); >> + >> +uint16_t bt_gatt_request_get_end_handle(struct bt_gatt_request *request); >> +uint16_t bt_gatt_request_get_start_handle(struct bt_gatt_request *request); >> -- >> 2.34.1 >> > > Best regards, Mengshi Wu ^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH v3 1/2] shared/att: Implement ATT error retry mechanism with callback support @ 2026-01-21 8:38 Mengshi Wu 2026-01-21 8:59 ` gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching bluez.test.bot 0 siblings, 1 reply; 6+ messages in thread From: Mengshi Wu @ 2026-01-21 8:38 UTC (permalink / raw) To: luiz.dentz Cc: linux-bluetooth, shuai.zhang, cheng.jiang, chezhou, wei.deng, yiboz, Mengshi Wu Add a retry mechanism for ATT operations that allows upper layers to decide whether to retry failed requests. This includes: - Add retry callback registration (bt_att_set_retry_cb) to allow applications to handle retry decisions - Implement pending_retry state tracking in att_send_op to store operations awaiting retry approval - Add bt_att_retry_request() and bt_att_cancel_retry() functions to approve or reject retry attempts - Store error PDUs during retry_pending state for callback inspection - Modify handle_error_rsp() to return retry decision codes instead of boolean values - Add BT_ATT_RETRY_* constants for retry decision handling - Update GATT helpers to support retry callbacks for operations like discovery and read/write requests This enables more robust error handling by allowing the application layer to implement custom retry logic based on ATT error codes. Signed-off-by: Mengshi Wu <mengshi.wu@oss.qualcomm.com> --- src/shared/att.c | 182 +++++++++++++++++++++++++++++++++++++++++++++-- src/shared/att.h | 16 +++++ 2 files changed, 191 insertions(+), 7 deletions(-) diff --git a/src/shared/att.c b/src/shared/att.c index 77ca4aa24..4ae97530a 100644 --- a/src/shared/att.c +++ b/src/shared/att.c @@ -47,6 +47,7 @@ struct bt_att_chan { struct att_send_op *pending_req; struct att_send_op *pending_ind; + struct att_send_op *pending_retry; bool writer_active; bool in_req; /* There's a pending incoming request */ @@ -78,6 +79,10 @@ struct bt_att { bt_att_destroy_func_t timeout_destroy; void *timeout_data; + bt_att_retry_func_t retry_callback; + bt_att_destroy_func_t retry_destroy; + void *retry_data; + uint8_t debug_level; bt_att_debug_func_t debug_callback; bt_att_destroy_func_t debug_destroy; @@ -194,6 +199,9 @@ struct att_send_op { void *pdu; uint16_t len; bool retry; + bool retry_pending; /* Waiting for approval to retry */ + uint8_t *error_pdu; /* Stored error PDU for retry_pending */ + uint16_t error_pdu_len; bt_att_response_func_t callback; bt_att_destroy_func_t destroy; void *user_data; @@ -210,6 +218,7 @@ static void destroy_att_send_op(void *data) op->destroy(op->user_data); free(op->pdu); + free(op->error_pdu); free(op); } @@ -644,6 +653,9 @@ static void bt_att_chan_free(void *data) if (chan->pending_ind) destroy_att_send_op(chan->pending_ind); + if (chan->pending_retry) + destroy_att_send_op(chan->pending_retry); + queue_destroy(chan->queue, destroy_att_send_op); io_destroy(chan->io); @@ -682,6 +694,11 @@ static bool disconnect_cb(struct io *io, void *user_data) chan->pending_ind = NULL; } + if (chan->pending_retry) { + disc_att_send_op(chan->pending_retry); + chan->pending_retry = NULL; + } + bt_att_chan_free(chan); /* Don't run disconnect callback if there are channels left */ @@ -777,16 +794,17 @@ static bool change_security(struct bt_att_chan *chan, uint8_t ecode) return bt_att_chan_set_security(chan, security); } -static bool handle_error_rsp(struct bt_att_chan *chan, uint8_t *pdu, +static int handle_error_rsp(struct bt_att_chan *chan, uint8_t *pdu, ssize_t pdu_len, uint8_t *opcode) { struct bt_att *att = chan->att; const struct bt_att_pdu_error_rsp *rsp; struct att_send_op *op = chan->pending_req; + int should_retry = BT_ATT_RETRY_NO; if (pdu_len != sizeof(*rsp)) { *opcode = 0; - return false; + return should_retry; } rsp = (void *) pdu; @@ -797,11 +815,43 @@ static bool handle_error_rsp(struct bt_att_chan *chan, uint8_t *pdu, * the security again. */ if (op->retry) - return false; + return should_retry; /* Attempt to change security */ - if (!change_security(chan, rsp->ecode)) - return false; + if (change_security(chan, rsp->ecode)) { + should_retry = BT_ATT_RETRY_YES; + } else if (att->retry_callback) { + should_retry = att->retry_callback(op->opcode, rsp->ecode, + op->pdu + 1, op->len - 1, + op->id, att->retry_data); + + /* Check if callback wants to defer the retry decision */ + if (should_retry == BT_ATT_RETRY_PENDING) { + op->retry_pending = true; + + /* Store error PDU for later use */ + op->error_pdu = malloc(pdu_len); + if (op->error_pdu) { + memcpy(op->error_pdu, pdu, pdu_len); + op->error_pdu_len = pdu_len; + } + + /* Remove timeout since we're waiting for approval */ + if (op->timeout_id) { + timeout_remove(op->timeout_id); + op->timeout_id = 0; + } + + /* Move from pending_req to pending_retry */ + chan->pending_retry = op; + + DBG(att, "(chan %p) Retry pending for operation %p", + chan, op); + } + } + + if (should_retry != BT_ATT_RETRY_YES) + return should_retry; /* Remove timeout_id if outstanding */ if (op->timeout_id) { @@ -815,7 +865,8 @@ static bool handle_error_rsp(struct bt_att_chan *chan, uint8_t *pdu, op->retry = true; /* Push operation back to channel queue */ - return queue_push_head(chan->queue, op); + return queue_push_head(chan->queue, op) ? + BT_ATT_RETRY_YES : BT_ATT_RETRY_NO; } static void handle_rsp(struct bt_att_chan *chan, uint8_t opcode, uint8_t *pdu, @@ -845,9 +896,15 @@ static void handle_rsp(struct bt_att_chan *chan, uint8_t opcode, uint8_t *pdu, */ if (opcode == BT_ATT_OP_ERROR_RSP) { /* Return if error response cause a retry */ - if (handle_error_rsp(chan, pdu, pdu_len, &req_opcode)) { + switch (handle_error_rsp(chan, pdu, pdu_len, &req_opcode)) { + case BT_ATT_RETRY_PENDING: + /* Operation moved to pending_retry, clear pending_req */ + chan->pending_req = NULL; + case BT_ATT_RETRY_YES: wakeup_chan_writer(chan, NULL); return; + default: + break; } } else if (!(req_opcode = get_req_opcode(opcode))) goto fail; @@ -1142,6 +1199,9 @@ static void bt_att_free(struct bt_att *att) if (att->timeout_destroy) att->timeout_destroy(att->timeout_data); + if (att->retry_destroy) + att->retry_destroy(att->retry_data); + if (att->debug_destroy) att->debug_destroy(att->debug_data); @@ -1473,6 +1533,23 @@ bool bt_att_set_timeout_cb(struct bt_att *att, bt_att_timeout_func_t callback, return true; } +bool bt_att_set_retry_cb(struct bt_att *att, bt_att_retry_func_t callback, + void *user_data, + bt_att_destroy_func_t destroy) +{ + if (!att) + return false; + + if (att->retry_destroy) + att->retry_destroy(att->retry_data); + + att->retry_callback = callback; + att->retry_destroy = destroy; + att->retry_data = user_data; + + return true; +} + unsigned int bt_att_register_disconnect(struct bt_att *att, bt_att_disconnect_func_t callback, void *user_data, @@ -2051,6 +2128,97 @@ bool bt_att_has_crypto(struct bt_att *att) return att->crypto ? true : false; } +bool bt_att_retry_request(struct bt_att *att, unsigned int id) +{ + const struct queue_entry *entry; + struct bt_att_chan *chan = NULL; + struct att_send_op *op; + + if (!att || !id) + return false; + + /* Find the channel with the pending retry operation */ + for (entry = queue_get_entries(att->chans); entry; + entry = entry->next) { + struct bt_att_chan *c = entry->data; + + if (c->pending_retry && c->pending_retry->id == id && + c->pending_retry->retry_pending) { + chan = c; + op = c->pending_retry; + break; + } + } + + if (!chan || !op) + return false; + + DBG(att, "(chan %p) Approving retry for operation %p", chan, op); + + /* Clear pending retry state and mark for retry */ + op->retry_pending = false; + op->retry = true; + chan->pending_retry = NULL; + + /* Free stored error PDU as we're retrying */ + free(op->error_pdu); + op->error_pdu = NULL; + op->error_pdu_len = 0; + + /* Push operation back to channel queue for retry */ + if (!queue_push_head(chan->queue, op)) + return false; + + /* Wake up writer to send the retry */ + wakeup_chan_writer(chan, NULL); + + return true; +} + +bool bt_att_cancel_retry(struct bt_att *att, unsigned int id) +{ + const struct queue_entry *entry; + struct bt_att_chan *chan = NULL; + struct att_send_op *op; + + if (!att || !id) + return false; + + /* Find the channel with the pending retry operation */ + for (entry = queue_get_entries(att->chans); entry; + entry = entry->next) { + struct bt_att_chan *c = entry->data; + + if (c->pending_retry && c->pending_retry->id == id && + c->pending_retry->retry_pending) { + chan = c; + op = c->pending_retry; + break; + } + } + + if (!chan || !op) + return false; + + DBG(att, "(chan %p) Canceling retry for operation %p", chan, op); + + /* Clear pending retry state */ + op->retry_pending = false; + chan->pending_retry = NULL; + + /* Call the callback with stored error PDU to notify upper layer */ + if (op->callback) + op->callback(BT_ATT_OP_ERROR_RSP, op->error_pdu, + op->error_pdu_len, op->user_data); + + destroy_att_send_op(op); + + /* Wake up writer in case there are other operations */ + wakeup_chan_writer(chan, NULL); + + return true; +} + bool bt_att_set_retry(struct bt_att *att, unsigned int id, bool retry) { struct att_send_op *op; diff --git a/src/shared/att.h b/src/shared/att.h index 53a3f7a2a..6dc9944bb 100644 --- a/src/shared/att.h +++ b/src/shared/att.h @@ -46,6 +46,15 @@ typedef void (*bt_att_disconnect_func_t)(int err, void *user_data); typedef void (*bt_att_exchange_func_t)(uint16_t mtu, void *user_data); typedef bool (*bt_att_counter_func_t)(uint32_t *sign_cnt, void *user_data); +/* Return values for bt_att_retry_func_t */ +#define BT_ATT_RETRY_NO 0 /* Don't retry */ +#define BT_ATT_RETRY_YES 1 /* Retry immediately */ +#define BT_ATT_RETRY_PENDING 2 /* Defer retry decision */ + +typedef int (*bt_att_retry_func_t)(uint8_t opcode, uint8_t error_code, + const void *pdu, uint16_t length, + unsigned int att_id, void *user_data); + bool bt_att_set_debug(struct bt_att *att, uint8_t level, bt_att_debug_func_t callback, void *user_data, bt_att_destroy_func_t destroy); @@ -58,6 +67,13 @@ bool bt_att_set_timeout_cb(struct bt_att *att, bt_att_timeout_func_t callback, void *user_data, bt_att_destroy_func_t destroy); +bool bt_att_set_retry_cb(struct bt_att *att, bt_att_retry_func_t callback, + void *user_data, + bt_att_destroy_func_t destroy); + +bool bt_att_retry_request(struct bt_att *att, unsigned int id); +bool bt_att_cancel_retry(struct bt_att *att, unsigned int id); + unsigned int bt_att_send(struct bt_att *att, uint8_t opcode, const void *pdu, uint16_t length, bt_att_response_func_t callback, -- 2.34.1 ^ permalink raw reply related [flat|nested] 6+ messages in thread
* RE: gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching. 2026-01-21 8:38 [PATCH v3 1/2] shared/att: Implement ATT error retry mechanism with callback support Mengshi Wu @ 2026-01-21 8:59 ` bluez.test.bot 0 siblings, 0 replies; 6+ messages in thread From: bluez.test.bot @ 2026-01-21 8:59 UTC (permalink / raw) To: linux-bluetooth, mengshi.wu [-- Attachment #1: Type: text/plain, Size: 5208 bytes --] This is automated email and please do not reply to this email! Dear submitter, Thank you for submitting the patches to the linux bluetooth mailing list. This is a CI test results with your patch series: PW Link:https://patchwork.kernel.org/project/bluetooth/list/?series=1045032 ---Test result--- Test Summary: CheckPatch PENDING 0.34 seconds GitLint PENDING 0.32 seconds BuildEll PASS 19.85 seconds BluezMake FAIL 13.55 seconds MakeCheck FAIL 25.40 seconds MakeDistcheck PASS 240.00 seconds CheckValgrind FAIL 11.82 seconds CheckSmatch FAIL 14.07 seconds bluezmakeextell FAIL 11.37 seconds IncrementalBuild PENDING 0.36 seconds ScanBuild FAIL 14.03 seconds Details ############################## Test: CheckPatch - PENDING Desc: Run checkpatch.pl script Output: ############################## Test: GitLint - PENDING Desc: Run gitlint Output: ############################## Test: BluezMake - FAIL Desc: Build BlueZ Output: src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7859: src/shared/libshared_mainloop_la-att.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: MakeCheck - FAIL Desc: Run Bluez Make Check Output: src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7586: src/shared/libshared_glib_la-att.lo] Error 1 make: *** [Makefile:11022: check] Error 2 ############################## Test: CheckValgrind - FAIL Desc: Run Bluez Make Check with Valgrind Output: src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7859: src/shared/libshared_mainloop_la-att.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:11022: check] Error 2 ############################## Test: CheckSmatch - FAIL Desc: Run smatch tool with source Output: src/shared/crypto.c:271:21: warning: Variable length array is used. src/shared/crypto.c:272:23: warning: Variable length array is used. src/shared/gatt-helpers.c:768:31: warning: Variable length array is used. src/shared/gatt-helpers.c:846:31: warning: Variable length array is used. src/shared/gatt-helpers.c:1339:31: warning: Variable length array is used. src/shared/gatt-helpers.c:1370:23: warning: Variable length array is used. src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7859: src/shared/libshared_mainloop_la-att.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: bluezmakeextell - FAIL Desc: Build Bluez with External ELL Output: src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7859: src/shared/libshared_mainloop_la-att.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 ############################## Test: IncrementalBuild - PENDING Desc: Incremental build with the patches in the series Output: ############################## Test: ScanBuild - FAIL Desc: Run Scan Build Output: src/shared/att.c: In function ‘handle_rsp’: src/shared/att.c:902:22: error: this statement may fall through [-Werror=implicit-fallthrough=] 902 | chan->pending_req = NULL; | ^ src/shared/att.c:903:3: note: here 903 | case BT_ATT_RETRY_YES: | ^~~~ cc1: all warnings being treated as errors make[1]: *** [Makefile:7859: src/shared/libshared_mainloop_la-att.lo] Error 1 make[1]: *** Waiting for unfinished jobs.... make: *** [Makefile:4243: all] Error 2 --- Regards, Linux Bluetooth ^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-01-21 8:59 UTC | newest] Thread overview: 6+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-01-05 10:38 [PATCH v2 0/1] gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching Mengshi Wu 2026-01-05 10:38 ` [PATCH v2 1/1] " Mengshi Wu 2026-01-05 10:54 ` bluez.test.bot 2026-01-05 19:21 ` [PATCH v2 1/1] " Luiz Augusto von Dentz 2026-01-06 6:00 ` Mengshi Wu -- strict thread matches above, loose matches on Subject: below -- 2026-01-21 8:38 [PATCH v3 1/2] shared/att: Implement ATT error retry mechanism with callback support Mengshi Wu 2026-01-21 8:59 ` gatt-client:Implement error handling for DB_OUT_OF_SYNC in GATT caching bluez.test.bot
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox