Skip to content

Commit fc9d608

Browse files
committed
WriteClient: Encoding as many items as possible into a single list as part of ReplaceAll item operation
1 parent dafe868 commit fc9d608

File tree

4 files changed

+257
-67
lines changed

4 files changed

+257
-67
lines changed

src/app/WriteClient.cpp

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ CHIP_ERROR WriteClient::StartNewMessage()
187187
reservedSize = static_cast<uint16_t>(reservedSize + Crypto::CHIP_CRYPTO_AEAD_MIC_LENGTH_BYTES);
188188

189189
// ... and the overhead for end of AttributeDataIBs (end of container), more chunks flag, end of WriteRequestMessage (another
190-
// end of container).
190+
// end of container), the End of the List (EndOfContainer as well) and End of AttributeDataIB (EndOfContainer)
191191
reservedSize = static_cast<uint16_t>(reservedSize + kReservedSizeForTLVEncodingOverhead);
192192

193193
#if CONFIG_BUILD_FOR_HOST_UNIT_TEST

src/app/WriteClient.h

+149-17
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141
#include <system/SystemPacketBuffer.h>
4242
#include <system/TLVPacketBufferBackingStore.h>
4343

44+
#include <app-common/zap-generated/ids/Attributes.h>
45+
#include <app-common/zap-generated/ids/Clusters.h>
4446
namespace chip {
4547
namespace app {
4648

@@ -162,25 +164,71 @@ class WriteClient : public Messaging::ExchangeDelegate
162164

163165
/**
164166
* Encode a possibly-chunked list attribute value. Will create a new chunk when necessary.
167+
*
168+
* This function will Attempt to to encode as many list items as possible into a SingleAttributeDataIB, which will be handled by
169+
cluster server as a ReplaceAll Item operation
170+
* If the list is too large, WriteRequest will be chunked and remaining items will be encoded as AppendItem operations, chunking
171+
them as needed
172+
*
173+
// The items in this list will be decoded by servers as part of the ReplaceAll list.
174+
165175
*/
166176
template <class T>
167-
CHIP_ERROR EncodeAttribute(const AttributePathParams & attributePath, const DataModel::List<T> & value,
177+
CHIP_ERROR EncodeAttribute(const AttributePathParams & attributePath, const DataModel::List<T> & listValue,
168178
const Optional<DataVersion> & aDataVersion = NullOptional)
169179
{
170180
// Here, we are using kInvalidEndpointId for missing endpoint id, which is used when sending group write requests.
171181
ConcreteDataAttributePath path =
172182
ConcreteDataAttributePath(attributePath.HasWildcardEndpointId() ? kInvalidEndpointId : attributePath.mEndpointId,
173183
attributePath.mClusterId, attributePath.mAttributeId, aDataVersion);
174184

175-
ReturnErrorOnFailure(EnsureMessage());
185+
ListIndex nextItemToAppendIndex = kInvalidListIndex;
186+
// TODO CHANGE NAME OF numEncodedItems and maybe not make it listindex?
187+
uint16_t numSuccessfullyEncodedItems = kInvalidListIndex;
188+
bool chunkingNeeded = false;
189+
190+
VerifyOrReturnError(!listValue.empty(), CHIP_ERROR_INVALID_LIST_LENGTH);
191+
192+
// BACKWARD COMPATIBILITY: Legacy OTA Requestor Servers( Pre-Matter 1.4) only support having an empty list for ReplaceAll
193+
// when writing to the DefaultOTAProviders attribute and they will only Decode List Items that are of the AppendItem List
194+
// operation. So we must allow this for backward compatiblity.
195+
// TODO remove this special case when Matter 1.3 is Sunsetted
196+
bool encodeEmptyListAsReplaceAll =
197+
(path.mClusterId == Clusters::OtaSoftwareUpdateRequestor::Id &&
198+
path.mAttributeId == Clusters::OtaSoftwareUpdateRequestor::Attributes::DefaultOTAProviders::Id);
199+
200+
if (encodeEmptyListAsReplaceAll)
201+
{
202+
ReturnErrorOnFailure(EnsureMessage());
203+
ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, DataModel::List<uint8_t>()));
204+
}
205+
else
206+
{
207+
// StartNewMessage was called to make sure we always create a new chunk when we have a new Attribute to Encode, this
208+
// might be more efficient
209+
// TODO should we keep this or just use EnsureMessage; if we choose EnsureMessage, we will have to adapt
210+
// EnsureListStarted to make sure we create a New Chunk if it fails (could happens when a new Attribute is added)
211+
ReturnErrorOnFailure(StartNewMessage());
176212

177-
// Encode an empty list for the chunking protocol.
178-
ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, DataModel::List<uint8_t>()));
213+
// TODO CHANGE NAME OF numEncodedItems
214+
ReturnErrorOnFailure(
215+
TryEncodeListIntoSingleAttributeDataIB(path, listValue, chunkingNeeded, numSuccessfullyEncodedItems));
216+
if (!chunkingNeeded)
217+
{
218+
return CHIP_NO_ERROR;
219+
}
220+
// Start a new WriteRequest chunk.
221+
ReturnErrorOnFailure(StartNewMessage());
222+
nextItemToAppendIndex = numSuccessfullyEncodedItems;
223+
}
179224

180225
path.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem;
181-
for (size_t i = 0; i < value.size(); i++)
226+
227+
ListIndex startIndex = encodeEmptyListAsReplaceAll ? 0 : nextItemToAppendIndex;
228+
229+
for (ListIndex i = startIndex; i < listValue.size(); i++)
182230
{
183-
ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, value.data()[i]));
231+
ReturnErrorOnFailure(EncodeSingleAttributeDataIB(path, listValue[i]));
184232
}
185233

186234
return CHIP_NO_ERROR;
@@ -254,31 +302,108 @@ class WriteClient : public Messaging::ExchangeDelegate
254302
/**
255303
* Encode an attribute value that can be directly encoded using DataModel::Encode.
256304
*/
257-
template <class T>
305+
template <class T, std::enable_if_t<!DataModel::IsFabricScoped<T>::value, int> = 0>
258306
CHIP_ERROR TryEncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value)
259307
{
260308
chip::TLV::TLVWriter * writer = nullptr;
261309

262310
ReturnErrorOnFailure(PrepareAttributeIB(attributePath));
263311
VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE);
264-
// Use if constexpr to make compile-time decision on which encoding method to use
265-
if constexpr (DataModel::IsFabricScoped<T>::value)
266-
{
267-
ReturnErrorOnFailure(
268-
DataModel::EncodeForWrite(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value));
269-
}
270-
else
312+
ReturnErrorOnFailure(DataModel::Encode(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value));
313+
ReturnErrorOnFailure(FinishAttributeIB());
314+
315+
return CHIP_NO_ERROR;
316+
}
317+
318+
template <class T, std::enable_if_t<DataModel::IsFabricScoped<T>::value, int> = 0>
319+
CHIP_ERROR TryEncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value)
320+
{
321+
chip::TLV::TLVWriter * writer = nullptr;
322+
323+
ReturnErrorOnFailure(PrepareAttributeIB(attributePath));
324+
VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE);
325+
ReturnErrorOnFailure(
326+
DataModel::EncodeForWrite(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value));
327+
ReturnErrorOnFailure(FinishAttributeIB());
328+
329+
return CHIP_NO_ERROR;
330+
}
331+
332+
CHIP_ERROR EnsureListStarted(const ConcreteDataAttributePath & attributePath)
333+
{
334+
chip::TLV::TLVWriter * writer = nullptr;
335+
336+
TLV::TLVType outerType;
337+
338+
ReturnErrorOnFailure(PrepareAttributeIB(attributePath));
339+
VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE);
340+
ReturnErrorOnFailure(
341+
writer->StartContainer(chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), TLV::kTLVType_Array, outerType));
342+
343+
VerifyOrDie(outerType == kAttributeDataIBType);
344+
345+
return CHIP_NO_ERROR;
346+
}
347+
348+
CHIP_ERROR EnsureListEnded()
349+
{
350+
chip::TLV::TLVWriter * writer = nullptr;
351+
VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE);
352+
353+
// In the event of Chunking (we have a CHIP_ERROR_NO_MEMORY), we need to Unreserve two more Bytes in order to be able to
354+
// Append EndOfContainer of the List + EndOfContainer of the AttributeDataIB
355+
VerifyOrDie(writer->UnreserveBuffer(kReservedSizeForEndOfListContainer + kReservedSizeForEndOfAttributeDataIB) ==
356+
CHIP_NO_ERROR);
357+
ReturnErrorOnFailure(writer->EndContainer(kAttributeDataIBType));
358+
ReturnErrorOnFailure(FinishAttributeIB());
359+
360+
return CHIP_NO_ERROR;
361+
}
362+
363+
template <class T>
364+
CHIP_ERROR TryEncodeListIntoSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath,
365+
const DataModel::List<T> & list, bool & chunkingNeeded,
366+
ListIndex & numSuccessfullyEncodedItems)
367+
{
368+
CHIP_ERROR err = CHIP_NO_ERROR;
369+
TLV::TLVWriter backupWriter;
370+
371+
ReturnErrorOnFailure(EnsureListStarted(attributePath));
372+
373+
chip::TLV::TLVWriter * writer = nullptr;
374+
VerifyOrReturnError((writer = GetAttributeDataIBTLVWriter()) != nullptr, CHIP_ERROR_INCORRECT_STATE);
375+
376+
numSuccessfullyEncodedItems = 0;
377+
378+
for (auto & item : list)
271379
{
272-
ReturnErrorOnFailure(DataModel::Encode(*writer, chip::TLV::ContextTag(chip::app::AttributeDataIB::Tag::kData), value));
380+
mWriteRequestBuilder.GetWriteRequests().Checkpoint(backupWriter);
381+
if constexpr (DataModel::IsFabricScoped<T>::value)
382+
{
383+
err = DataModel::EncodeForWrite(*writer, TLV::AnonymousTag(), item);
384+
}
385+
else
386+
{
387+
err = DataModel::Encode(*writer, TLV::AnonymousTag(), item);
388+
}
389+
390+
if (err == CHIP_ERROR_NO_MEMORY || err == CHIP_ERROR_BUFFER_TOO_SMALL)
391+
{
392+
mWriteRequestBuilder.GetWriteRequests().Rollback(backupWriter);
393+
chunkingNeeded = true;
394+
break;
395+
}
396+
ReturnErrorOnFailure(err);
397+
numSuccessfullyEncodedItems++;
273398
}
274-
ReturnErrorOnFailure(FinishAttributeIB());
275399

400+
ReturnErrorOnFailure(EnsureListEnded());
276401
return CHIP_NO_ERROR;
277402
}
278403

279404
/**
280405
* A wrapper for TryEncodeSingleAttributeDataIB which will start a new chunk when failed with CHIP_ERROR_NO_MEMORY or
281-
* CHIP_ERROR_BUFFER_TOO_SMALL.
406+
* CHIP_ERROR_BUFFER_TOO_SMALL. This will not be used for Lists
282407
*/
283408
template <class T>
284409
CHIP_ERROR EncodeSingleAttributeDataIB(const ConcreteDataAttributePath & attributePath, const T & value)
@@ -418,6 +543,13 @@ class WriteClient : public Messaging::ExchangeDelegate
418543
static constexpr uint16_t kReservedSizeForTLVEncodingOverhead = kReservedSizeForIMRevision + kReservedSizeForMoreChunksFlag +
419544
kReservedSizeForEndOfContainer + kReservedSizeForEndOfContainer;
420545
bool mHasDataVersion = false;
546+
547+
// This was not added to kReservedSizeForTLVEncodingOverhead since it will be "Unreserved" before the others.
548+
static constexpr uint32_t kReservedSizeForEndOfListContainer = 1;
549+
// TODO should this be 2 ?
550+
static constexpr uint32_t kReservedSizeForEndOfAttributeDataIB = 1;
551+
552+
static constexpr TLV::TLVType kAttributeDataIBType = TLV::kTLVType_Structure;
421553
};
422554

423555
} // namespace app

src/app/tests/TestWriteInteraction.cpp

+12-8
Original file line numberDiff line numberDiff line change
@@ -627,20 +627,24 @@ TEST_F_FROM_FIXTURE(TestWriteInteraction, TestWriteHandlerReceiveInvalidMessage)
627627
auto * engine = chip::app::InteractionModelEngine::GetInstance();
628628
EXPECT_EQ(engine->Init(&GetExchangeManager(), &GetFabricTable(), app::reporting::GetDefaultReportScheduler()), CHIP_NO_ERROR);
629629

630-
// Reserve all except the last 128 bytes, so that we make sure to chunk.
630+
// Reserve all except the last 96 bytes, so that we make sure to chunk.
631631
app::WriteClient writeClient(&GetExchangeManager(), &writeCallback, Optional<uint16_t>::Missing(),
632-
static_cast<uint16_t>(kMaxSecureSduLengthBytes - 128) /* reserved buffer size */);
632+
static_cast<uint16_t>(kMaxSecureSduLengthBytes - 96) /* reserved buffer size */);
633633

634-
ByteSpan list[5];
634+
// it was empirically determined that we need a list of at least 25 items to make sure we chunk (when the reserved buffer size =
635+
// kMaxSecureSduLengthBytes - 96)
636+
constexpr uint8_t kTestListLength = 30;
637+
ByteSpan list[kTestListLength];
635638

636-
EXPECT_EQ(writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, 5)), CHIP_NO_ERROR);
639+
EXPECT_EQ(writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, kTestListLength)), CHIP_NO_ERROR);
637640

638641
GetLoopback().mSentMessageCount = 0;
639642
GetLoopback().mNumMessagesToDrop = 1;
640643
GetLoopback().mNumMessagesToAllowBeforeDropping = 2;
641644
EXPECT_EQ(writeClient.SendWriteRequest(sessionHandle), CHIP_NO_ERROR);
642645
DrainAndServiceIO();
643646

647+
// TODO failure here, we have NULLPTR ERROR or a segmentation fault
644648
EXPECT_EQ(InteractionModelEngine::GetInstance()->GetNumActiveWriteHandlers(), 1u);
645649
EXPECT_EQ(GetLoopback().mSentMessageCount, 3u);
646650
EXPECT_EQ(GetLoopback().mDroppedMessageCount, 1u);
@@ -692,13 +696,13 @@ TEST_F(TestWriteInteraction, TestWriteHandlerInvalidateFabric)
692696
auto * engine = chip::app::InteractionModelEngine::GetInstance();
693697
EXPECT_EQ(engine->Init(&GetExchangeManager(), &GetFabricTable(), app::reporting::GetDefaultReportScheduler()), CHIP_NO_ERROR);
694698

695-
// Reserve all except the last 128 bytes, so that we make sure to chunk.
699+
// Reserve all except the last 96 bytes, so that we make sure to chunk.
696700
app::WriteClient writeClient(&GetExchangeManager(), &writeCallback, Optional<uint16_t>::Missing(),
697-
static_cast<uint16_t>(kMaxSecureSduLengthBytes - 128) /* reserved buffer size */);
701+
static_cast<uint16_t>(kMaxSecureSduLengthBytes - 96) /* reserved buffer size */);
698702

699-
ByteSpan list[5];
703+
ByteSpan list[30];
700704

701-
EXPECT_EQ(writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, 5)), CHIP_NO_ERROR);
705+
EXPECT_EQ(writeClient.EncodeAttribute(attributePath, app::DataModel::List<ByteSpan>(list, 30)), CHIP_NO_ERROR);
702706

703707
GetLoopback().mDroppedMessageCount = 0;
704708
GetLoopback().mSentMessageCount = 0;

0 commit comments

Comments
 (0)