[IM] Implement list chunking feature (#12019)
* Update AttributeValueEncoder
* TagBoundEncoder -> ListEncodeHelper
* Fix Null ListIndex support
* Offload build logic to separate class
* Add attribute for testing list chunking
* Fix Error
* Add even more tests
* auto -> const auto *
* Address comments
* Update
* Update Encode
* Update
* Update
* Update
* Fix style
* Fix
* Run Codegen
diff --git a/src/app/AttributeAccessInterface.h b/src/app/AttributeAccessInterface.h
index 8f37e99..9f7cca7 100644
--- a/src/app/AttributeAccessInterface.h
+++ b/src/app/AttributeAccessInterface.h
@@ -18,8 +18,9 @@
#pragma once
+#include <app/ClusterInfo.h>
#include <app/ConcreteAttributePath.h>
-#include <app/MessageDef/AttributeDataIB.h>
+#include <app/MessageDef/AttributeReportIBs.h>
#include <app/data-model/Decode.h>
#include <app/data-model/Encode.h>
#include <app/data-model/List.h> // So we can encode lists
@@ -41,44 +42,158 @@
namespace chip {
namespace app {
-class AttributeValueEncoder : protected TagBoundEncoder
+/**
+ * The AttributeReportBuilder is a helper class for filling a single report in AttributeReportIBs.
+ *
+ * Possible usage of AttributeReportBuilder might be:
+ *
+ * AttributeReportBuilder builder;
+ * ReturnErrorOnFailure(builder.PrepareAttribute(...));
+ * ReturnErrorOnFailure(builder.Encode(...));
+ * ReturnErrorOnFailure(builder.FinishAttribute());
+ */
+class AttributeReportBuilder
{
public:
- AttributeValueEncoder(TLV::TLVWriter * aWriter, FabricIndex aAccessingFabricIndex) :
- TagBoundEncoder(aWriter, TLV::ContextTag(to_underlying(AttributeDataIB::Tag::kData))),
- mAccessingFabricIndex(aAccessingFabricIndex)
+ /**
+ * PrepareAttribute encodes the "header" part of an attribute report including the path and data version.
+ * Path will be encoded according to section 10.5.4.3.1 in the spec.
+ * Note: Only append is supported currently (encode a null list index), other operations won't encode a list index in the
+ * attribute path field.
+ * TODO: Add support for encoding a single element in the list (path with a valid list index).
+ */
+ CHIP_ERROR PrepareAttribute(AttributeReportIBs::Builder & aAttributeReportIBs, const ConcreteDataAttributePath & aPath,
+ DataVersion aDataVersion);
+
+ /**
+ * FinishAttribute encodes the "footer" part of an attribute report (it closes the containers opened in PrepareAttribute)
+ */
+ CHIP_ERROR FinishAttribute();
+
+ /**
+ * EncodeValue encodes the value field of the report, it should be called exactly once.
+ */
+ template <typename... Ts>
+ CHIP_ERROR EncodeValue(Ts... aArgs)
+ {
+ return DataModel::Encode(*mAttributeDataIBBuilder.GetWriter(), TLV::ContextTag(to_underlying(AttributeDataIB::Tag::kData)),
+ std::forward<Ts>(aArgs)...);
+ }
+
+private:
+ AttributeReportIB::Builder mAttributeReportIBBuilder;
+ AttributeDataIB::Builder mAttributeDataIBBuilder;
+};
+
+/**
+ * The AttributeValueEncoder is a helper class for filling report payloads into AttributeReportIBs.
+ * The attribute value encoder can be initialized with a AttributeEncodeState for saving and recovering its state between encode
+ * sessions (chunkings).
+ *
+ * When Encode returns recoverable errors (e.g. CHIP_ERROR_NO_MEMORY) the state can be used to initialize the AttributeValueEncoder
+ * for future use on the same attribute path.
+ */
+class AttributeValueEncoder
+{
+public:
+ class ListEncodeHelper
+ {
+ public:
+ ListEncodeHelper(AttributeValueEncoder & encoder) : mAttributeValueEncoder(encoder) {}
+
+ template <typename... Ts>
+ CHIP_ERROR Encode(Ts... aArgs) const
+ {
+ return mAttributeValueEncoder.EncodeListItem(std::forward<Ts>(aArgs)...);
+ }
+
+ private:
+ AttributeValueEncoder & mAttributeValueEncoder;
+ };
+
+ class AttributeEncodeState
+ {
+ public:
+ AttributeEncodeState() : mAllowPartialData(false), mCurrentEncodingListIndex(kInvalidListIndex) {}
+ bool AllowPartialData() const { return mAllowPartialData; }
+
+ private:
+ friend class AttributeValueEncoder;
+ /**
+ * When an attempt to encode an attribute returns an error, the buffer may contain tailing dirty data
+ * (since the put was aborted). The report engine normally rolls back the buffer to right before encoding
+ * of the attribute started on errors.
+ *
+ * When chunking a list, EncodeListItem will atomically encode list items, ensuring that the
+ * state of the buffer is valid to send (i.e. contains no trailing garbage), and return an error
+ * if the list doesn't entirely fit. In this situation, mAllowPartialData is set to communicate to the
+ * report engine that it should not roll back the list items.
+ *
+ * TODO: There might be a better name for this variable.
+ */
+ bool mAllowPartialData = false;
+ /**
+ * If set to kInvalidListIndex, indicates that we have not encoded any data for the list yet and
+ * need to start by encoding an empty list before we start encoding any list items.
+ *
+ * When set to a valid ListIndex value, indicates the index of the next list item that needs to be
+ * encoded (i.e. the count of items encoded so far).
+ */
+ ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
+ };
+
+ AttributeValueEncoder(AttributeReportIBs::Builder & aAttributeReportIBsBuilder, FabricIndex aAccessingFabricIndex,
+ const ConcreteAttributePath & aPath, DataVersion aDataVersion,
+ const AttributeEncodeState & aState = AttributeEncodeState()) :
+ mAttributeReportIBsBuilder(aAttributeReportIBsBuilder),
+ mAccessingFabricIndex(aAccessingFabricIndex), mPath(aPath.mEndpointId, aPath.mClusterId, aPath.mAttributeId),
+ mDataVersion(aDataVersion), mEncodeState(aState)
{}
+ /**
+ * Encode builds a single AttributeReportIB in AttributeReportIBs.
+ * When we are encoding a single element in the list, the actual path in the report contains a null list index as "append"
+ * operation.
+ */
template <typename... Ts>
CHIP_ERROR Encode(Ts... aArgs)
{
mTriedEncode = true;
- if (mWriter == nullptr)
- {
- return CHIP_NO_ERROR;
- }
- return TagBoundEncoder::Encode(std::forward<Ts>(aArgs)...);
+ return EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
}
/**
- * aCallback is expected to take a const TagBoundEncoder& argument and
- * Encode() on it as many times as needed to encode all the list elements
- * one by one. If any of those Encode() calls returns failure, aCallback
- * must stop encoding and return failure. When all items are encoded
- * aCallback is expected to return success.
+ * aCallback is expected to take a const auto & argument and Encode() on it as many times as needed to encode all the list
+ * elements one by one. If any of those Encode() calls returns failure, aCallback must stop encoding and return failure. When
+ * all items are encoded aCallback is expected to return success.
*
- * aCallback may not be called. Consumers must not assume it will be
- * called.
+ * aCallback may not be called. Consumers must not assume it will be called.
+ *
+ * When EncodeList returns an error, the consumers must abort the encoding, and return the exact error to the caller.
+ *
+ * TODO: Can we hold a error state in the AttributeValueEncoder itself so functions in ember-compatibility-functions don't have
+ * to rely on the above assumption?
+ *
+ * Consumers are allowed to make either one call to EncodeList or one call to Encode to handle a read.
+ *
*/
template <typename ListGenerator>
CHIP_ERROR EncodeList(ListGenerator aCallback)
{
mTriedEncode = true;
- if (mWriter == nullptr)
- {
- return CHIP_NO_ERROR;
- }
- return TagBoundEncoder::EncodeList(aCallback);
+ // Spec 10.5.4.3.1, 10.5.4.6 (Replace a list w/ Multiple IBs)
+ // EmptyList acts as the beginning of the whole array type attribute report.
+ // An empty list is encoded iff both mCurrentEncodingListIndex and mEncodeState.mCurrentEncodingListIndex are invalid
+ // values. After encoding the empty list, mEncodeState.mCurrentEncodingListIndex and mCurrentEncodingListIndex are set to 0.
+ mPath.mListOp = ConcreteDataAttributePath::ListOperation::ReplaceAll;
+ ReturnErrorOnFailure(EncodeEmptyList());
+ // For all elements in the list, a report with append operation will be generated. This will not be changed during encoding
+ // of each report since the users cannot access mPath.
+ mPath.mListOp = ConcreteDataAttributePath::ListOperation::AppendItem;
+ ReturnErrorOnFailure(aCallback(ListEncodeHelper(*this)));
+ // The Encode procedure finished without any error, clear the state.
+ mEncodeState = AttributeEncodeState();
+ return CHIP_NO_ERROR;
}
bool TriedEncode() const { return mTriedEncode; }
@@ -88,18 +203,75 @@
*/
FabricIndex AccessingFabricIndex() const { return mAccessingFabricIndex; }
- // For consumers that can't just do a single Encode call for some reason
- // (e.g. they're encoding a list a bit at a time).
- TLV::TLVWriter * PrepareManualEncode()
- {
- // If this is called, the consumer is trying to encode a value.
- mTriedEncode = true;
- return mWriter;
- }
+ /**
+ * AttributeValueEncoder is a short lived object, and the state is persisted by mEncodeState and restored by constructor.
+ */
+ const AttributeEncodeState & GetState() const { return mEncodeState; }
private:
+ // We made EncodeListItem() private, and ListEncoderHelper will expose it by Encode()
+ friend class ListEncodeHelper;
+
+ template <typename... Ts>
+ CHIP_ERROR EncodeListItem(Ts... aArgs)
+ {
+ // EncodeListItem must be called after EncodeEmptyList(), thus mCurrentEncodingListIndex and
+ // mEncodeState.mCurrentEncodingListIndex are not invalid values.
+ if (mCurrentEncodingListIndex < mEncodeState.mCurrentEncodingListIndex)
+ {
+ // We have encoded this element in previous chunks, skip it.
+ mCurrentEncodingListIndex++;
+ return CHIP_NO_ERROR;
+ }
+
+ TLV::TLVWriter backup;
+ mAttributeReportIBsBuilder.Checkpoint(backup);
+
+ CHIP_ERROR err = EncodeAttributeReportIB(std::forward<Ts>(aArgs)...);
+ if (err != CHIP_NO_ERROR)
+ {
+ // For list chunking, ReportEngine should not rollback the buffer when CHIP_NO_MEMORY or similar error occurred.
+ // However, the error might be raised in the middle of encoding procedure, then the buffer may contain partial data,
+ // unclosed containers etc. This line clears all possible partial data and makes EncodeListItem is atomic.
+ mAttributeReportIBsBuilder.Rollback(backup);
+ return err;
+ }
+
+ mCurrentEncodingListIndex++;
+ mEncodeState.mCurrentEncodingListIndex++;
+ return CHIP_NO_ERROR;
+ }
+
+ /**
+ * Actual logic for encoding a single AttributeReportIB in AttributeReportIBs.
+ */
+ template <typename... Ts>
+ CHIP_ERROR EncodeAttributeReportIB(Ts... aArgs)
+ {
+ mTriedEncode = true;
+ AttributeReportBuilder builder;
+
+ ReturnErrorOnFailure(builder.PrepareAttribute(mAttributeReportIBsBuilder, mPath, mDataVersion));
+ ReturnErrorOnFailure(builder.EncodeValue(std::forward<Ts>(aArgs)...));
+
+ return builder.FinishAttribute();
+ }
+
+ /**
+ * EncodeEmptyList encodes the first item of one report with lists (an empty list).
+ *
+ * If internal state indicates we have already encoded the empty list, this function will encode nothing, set
+ * mCurrentEncodingListIndex to 0 and return CHIP_NO_ERROR.
+ */
+ CHIP_ERROR EncodeEmptyList();
+
bool mTriedEncode = false;
+ AttributeReportIBs::Builder & mAttributeReportIBsBuilder;
const FabricIndex mAccessingFabricIndex;
+ ConcreteDataAttributePath mPath;
+ DataVersion mDataVersion;
+ AttributeEncodeState mEncodeState;
+ ListIndex mCurrentEncodingListIndex = kInvalidListIndex;
};
class AttributeValueDecoder