Skip to content

[SPARK-46928][SS] Add support for ListState in Arbitrary State API v2. - #44961

Closed
sahnib wants to merge 2 commits into
apache:masterfrom
sahnib:state-api-v2-list-state
Closed

sahnib wants to merge 2 commits into
apache:masterfrom
sahnib:state-api-v2-list-state

Conversation

@sahnib

@sahnib sahnib commented Jan 31, 2024 •

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR adds changes for ListState implementation in State Api v2. As a list contains multiple values for a single key, we utilize RocksDB merge operator to persist multiple values.

Changes include

  1. A new encoder/decoder to encode multiple values inside a single byte[] array (stored in RocksDB). The encoding scheme is compatible with RocksDB StringAppendOperator merge operator.
  2. Support merge operations in ChangelogCheckpointing v2.
  3. Extend StateStore to support merge operation, and read multiple values for a single key (via a Iterator). Note that these changes are only supported for RocksDB currently.

Why are the changes needed?

These changes are needed to support list values in the State Store. The changes are part of the work around adding new stateful streaming operator for arbitrary state mgmt that provides a bunch of new features listed in the SPIP JIRA here - https://issues.apache.org/jira/browse/SPARK-45939

Does this PR introduce any user-facing change?

Yes
This PR introduces a new state type (ListState) that users can use in their Spark streaming queries.

How was this patch tested?

  1. Added a new test suite for ListState to ensure the state produces correct results.
  2. Added additional testcases for input validation.
  3. Added tests for merge operator with RocksDB.
  4. Added tests for changelog checkpointing merge operator.
  5. Added tests for reading merged values in RocksDBStateStore.

Was this patch authored or co-authored using generative AI tooling?

No

@ericm-db ericm-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only nits, most points were addressed on the previous PR.

@sahnib
sahnib force-pushed the state-api-v2-list-state branch from 4eec351 to 2a0602c Compare January 31, 2024 22:50
@sahnib
sahnib force-pushed the state-api-v2-list-state branch 2 times, most recently from d358120 to fa137f8 Compare February 1, 2024 22:11
@sahnib
sahnib force-pushed the state-api-v2-list-state branch from fa137f8 to 25e2230 Compare February 1, 2024 23:46
@sahnib
sahnib force-pushed the state-api-v2-list-state branch from 25e2230 to e259dd4 Compare February 2, 2024 02:52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe move arguments to new lines ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the first argument does not seem to be moved

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved.

@sahnib
sahnib force-pushed the state-api-v2-list-state branch 2 times, most recently from 1f943d5 to 68186c4 Compare February 6, 2024 18:13
Comment thread sql/api/src/main/scala/org/apache/spark/sql/streaming/ListState.scala Outdated
@sahnib
sahnib force-pushed the state-api-v2-list-state branch from 68186c4 to dffe870 Compare February 6, 2024 21:20
@sahnib
sahnib force-pushed the state-api-v2-list-state branch from a0c6457 to 2e346de Compare February 8, 2024 22:05
Comment thread sql/api/src/main/scala/org/apache/spark/sql/streaming/ListState.scala Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just remove this ? cc - @HeartSaVioR

@sahnib
sahnib force-pushed the state-api-v2-list-state branch 2 times, most recently from 6a4b641 to 9e7750f Compare February 8, 2024 23:46

@anishshri-db anishshri-db left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm ! thanks

@HeartSaVioR HeartSaVioR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First pass. Looks OK in general. Will have a second review once we merge the PR #45038 and this PR is rebased.

StateStoreProvider.createAndInit(
stateStoreProviderId, keySchema, valueSchema, numColsPrefixKey,
useColumnFamilies = false, storeConf, hadoopConf.value)
useColumnFamilies = false, storeConf, hadoopConf.value, useMultipleValuesPerKey = false)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we file a JIRA ticket to support transformWithState with state data source - reader? From what I understand, we are deferring on supporting this. Please correct me if I'm missing.

@sahnib sahnib Feb 14, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we already have this planned. @anishshri-db Do we have the JIRA created for this support?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the first argument does not seem to be moved

class ListStateImpl[S](store: StateStore,
stateName: String,
keyExprEnc: ExpressionEncoder[Any])
extends ListState[S] with Logging {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can create serializer from keyExprEnc here as field and pass serializer instead in encodeGroupingKey.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

val keyByteArr = toRow
.apply(keyOption.get).asInstanceOf[UnsafeRow].getBytes()

val keyEncoder = UnsafeProjection.create(KEY_ROW_SCHEMA)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are fully sure we will end up with binary key - binary value for every state type, this should go up to fields in this object. Same for valueEncoder. If we anticipate different schema, maybe keyEncoder / valueEncoder should be passed as parameter instead.

@sahnib sahnib Feb 14, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not anticipate a different schema. Every key, value will be encoded to a byte Array. While working on refactoring key/value Encoder to move them to object level, I discovered that UnsafeProjection is not thread safe. Hence, we cannot use it across state variables.

Refactored this logic to create a class instead of utils object, and the class instance is not shared across state variables.

def remove(key: UnsafeRow,
colFamilyName: String = StateStore.DEFAULT_COL_FAMILY_NAME): Unit

def merge(key: UnsafeRow, value: UnsafeRow,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we add method doc?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, missed the doc here. Added.

@sahnib
sahnib force-pushed the state-api-v2-list-state branch 3 times, most recently from 092c75a to 00183db Compare February 14, 2024 19:50

@HeartSaVioR HeartSaVioR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only details and optional comments.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud - it'd be ideal if we can move out the context of schema for state store key/value entirely to StateTypesEncoderUtils. State impls don't need to know about anything except that StateTypesEncoderUtils will give the UnsafeRow, and all context about serde will be put to StateTypesEncoderUtils. Just a 2 cents, not mandatory to address.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we anticipate further changes to StateTypesEncoderUtils it's also OK to leave it as it is and make such change in later phase.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This schema is currently used in TransformWithStateExec operator to create the StateStoreRDD, in the state variables to create RocksDB column family, and inside the EncoderUtils for serde. We can unify the last 2 easily, and that way state impl classes would not need to know about the schema. It would be better to make this change with MapState finalized as that class will modify the stateTypesEncoder. Let me know if that sounds good.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we can defer to the later phase - it may give better visibility upon further changes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is beyond the scope of PR, but while we are here, I don't see any difference with Option(get()).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This triggers overhead so we wouldn't like to do this for every value. Either we have to sample (that's why we only do for the first value) or not doing this at all. It's probably OK to do latter as get() also doesn't do this for the case where there are multiple CFs.

@sahnib sahnib Feb 16, 2024 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the wrapper iterator, we don't need it anymore. Thanks for pointing this out.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make a contract about expected return value, especially about clarification of value for non-existing key. Multiple implementations should follow the contract so this needs to be clearly explained.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. Done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we want to explicitly document the contract on the behavior when performing merge against non-existing key, since all implementations should provide the same.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, done.

@sahnib
sahnib force-pushed the state-api-v2-list-state branch 3 times, most recently from 41dc298 to ebd9895 Compare February 16, 2024 20:24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I talked offline but to leave record (and not forget) -

The overhead of creating UnsafeProjection is significant and we really need to reuse this, with keeping in mind that the instance returned from UnsafeProjecion.create() would be thread-unsafe. So we should either 1) use thread-local or 2) just change this object (singleton) into class and initialize per state impl.

Aggressively you can even create it in operator-partition level so that multiple state impls can share without worrying about thread-safety (benefit of 1) but even without thread-local), but maybe it won't be a significant gain.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing out the overhead of UnsafeProjection. I changed the StateTypesEncoderUtils to a class and its initialized per state impl to reuse the UnsafeProjection (for a StateImpl).

@sahnib
sahnib force-pushed the state-api-v2-list-state branch 3 times, most recently from c6066d0 to 880b0ba Compare February 20, 2024 23:53
@sahnib
sahnib force-pushed the state-api-v2-list-state branch from 880b0ba to 92c263d Compare February 21, 2024 00:01

@HeartSaVioR HeartSaVioR left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 pending CI.

@HeartSaVioR

Copy link
Copy Markdown
Contributor

CI failed from Run / Build modules: pyspark-sql, pyspark-resource, pyspark-testing but none of test failures are relevant to this change.

@HeartSaVioR

Copy link
Copy Markdown
Contributor

Thanks! Merging to master.

@HeartSaVioR HeartSaVioR changed the title [SS][SPARK-46928] Add support for ListState in Arbitrary State API v2. [SPARK-46928][SS] Add support for ListState in Arbitrary State API v2. Feb 21, 2024
MaxGekk pushed a commit that referenced this pull request Mar 14, 2024
### What changes were proposed in this pull request?
The pr aims to remove some unused error classes, include:
- _LEGACY_ERROR_TEMP_3193: The code in PR(#44961) no longer uses this error class, but the related item in the file `error-classes.json` has not been deleted synchronously.
- _LEGACY_ERROR_TEMP_3197: The code in PR(#44961) no longer uses this error class, but the related item in the file `error-classes.json` has not been deleted synchronously.
- _LEGACY_ERROR_TEMP_3234: This should be the follow-up work of #44665
- STATE_STORE_MULTIPLE_VALUES_PER_KEY: Introduced by [SPARK-46864](#44883), but not actually used in codebase.
- TWS_VALUE_SHOULD_NOT_BE_NULL: Introduced by [SPARK-46864](#44883), but not actually used in codebase.

### Why are the changes needed?
Make the code cleaner.

### Does this PR introduce _any_ user-facing change?
No.

### How was this patch tested?
- Pass GA.
- Manually check.

### Was this patch authored or co-authored using generative AI tooling?
No.

Closes #45509 from panbingkun/remove_outdated_error_classes.

Authored-by: panbingkun <panbingkun@baidu.com>
Signed-off-by: Max Gekk <max.gekk@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants