Skip to content

Commit 10fc53c

Browse files
Introduce unsnapshottableIdentifier() to allow transient WorkflowIdentifiers.
This fixes #79 and unblocks the ability to implement GUWT Workers as Workflows. `WorkflowIdentifier` can now indicate that it is not serializable by returning null from `toByteString()`, in which case neither it nor the snapshot of the workflow it identifies will be serialized. Because these identifiers do not need to be serialized, they can contain arbitrary values such as `KType`s, which is how Workers distinguish themselves for example. Workers also do not need to be snapshotted, so this works out.
1 parent 77c9d21 commit 10fc53c

File tree

6 files changed

+295
-82
lines changed

6 files changed

+295
-82
lines changed

workflow-core/api/workflow-core.api

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,16 +168,16 @@ public final class com/squareup/workflow/WorkflowAction$Updater {
168168
public final fun setState (Ljava/lang/Object;)V
169169
}
170170

171-
public final class com/squareup/workflow/WorkflowIdentifier {
171+
public abstract class com/squareup/workflow/WorkflowIdentifier {
172172
public static final field Companion Lcom/squareup/workflow/WorkflowIdentifier$Companion;
173-
public fun equals (Ljava/lang/Object;)Z
174-
public fun hashCode ()I
173+
protected abstract fun getProxiedIdentifier ()Lcom/squareup/workflow/WorkflowIdentifier;
174+
protected abstract fun getTypeName ()Ljava/lang/String;
175+
public abstract fun maybeToByteString ()Lokio/ByteString;
175176
public fun toString ()Ljava/lang/String;
176-
public final fun write (Lokio/BufferedSink;)V
177177
}
178178

179179
public final class com/squareup/workflow/WorkflowIdentifier$Companion {
180-
public final fun read (Lokio/BufferedSource;)Lcom/squareup/workflow/WorkflowIdentifier;
180+
public final fun parse (Lokio/ByteString;)Lcom/squareup/workflow/WorkflowIdentifier;
181181
}
182182

183183
public final class com/squareup/workflow/WorkflowOutput {
@@ -225,6 +225,7 @@ public final class com/squareup/workflow/Workflows {
225225
public static synthetic fun stateful$default (Lcom/squareup/workflow/Workflow$Companion;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;ILjava/lang/Object;)Lcom/squareup/workflow/StatefulWorkflow;
226226
public static final fun stateless (Lcom/squareup/workflow/Workflow$Companion;Lkotlin/jvm/functions/Function2;)Lcom/squareup/workflow/Workflow;
227227
public static final fun transform (Lcom/squareup/workflow/Worker;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/Worker;
228+
public static final fun unsnapshottableIdentifier (Lkotlin/reflect/KType;)Lcom/squareup/workflow/WorkflowIdentifier;
228229
public static final fun workflowAction (Lcom/squareup/workflow/StatefulWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction;
229230
public static final fun workflowAction (Lcom/squareup/workflow/StatefulWorkflow;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow/WorkflowAction;
230231
public static synthetic fun workflowAction$default (Lcom/squareup/workflow/StatefulWorkflow;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow/WorkflowAction;

workflow-core/src/main/java/com/squareup/workflow/WorkflowIdentifier.kt

Lines changed: 116 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818

1919
package com.squareup.workflow
2020

21-
import okio.BufferedSink
22-
import okio.BufferedSource
21+
import okio.Buffer
22+
import okio.ByteString
2323
import okio.EOFException
2424
import kotlin.LazyThreadSafetyMode.PUBLICATION
2525
import kotlin.reflect.KClass
26+
import kotlin.reflect.KType
2627

2728
/**
2829
* Represents a [Workflow]'s "identity" and is used by the runtime to determine whether a workflow
@@ -31,55 +32,104 @@ import kotlin.reflect.KClass
3132
*
3233
* A workflow's identity consists primarily of its concrete type (i.e. the class that implements
3334
* the [Workflow] interface). Two workflows of the same concrete type are considered identical.
34-
*
3535
* However, if a workflow class implements [ImpostorWorkflow], the identifier will also include
3636
* that workflow's [ImpostorWorkflow.realIdentifier].
3737
*
38-
* @constructor
39-
* @param type The [KClass] of the [Workflow] this identifier identifies.
40-
* @param proxiedIdentifier An optional identifier from [ImpostorWorkflow.realIdentifier] that will
41-
* be used to further narrow the scope of this identifier.
38+
* Instances of this class are [equatable][equals] and [hashable][hashCode].
39+
*
40+
* ## Identifiers and snapshots
41+
*
42+
* Since workflows can be [serialized][StatefulWorkflow.snapshotState], workflows' identifiers must
43+
* also be serializable in order to match workflows back up with their snapshots when restoring.
44+
* However, some [WorkflowIdentifier]s may represent workflows that cannot be snapshotted. When an
45+
* identifier is not snapshottable, [maybeToByteString] will return null, and any identifiers that
46+
* reference [ImpostorWorkflow]s whose [ImpostorWorkflow.realIdentifier] is not snapshottable will
47+
* also not be snapshottable. Such identifiers are created with [unsnapshottableIdentifier], but
48+
* should not be used to wrap arbitrary workflows since those workflows may expect to be
49+
* snapshotted.
4250
*/
4351
@ExperimentalWorkflowApi
44-
class WorkflowIdentifier internal constructor(
45-
private val type: KClass<out Workflow<*, *, *>>,
46-
private val proxiedIdentifier: WorkflowIdentifier?
47-
) {
52+
// TODO not sure i like having protected members, those are part of the ABI – can we make this implementation
53+
// entirely module-private somehow?
54+
sealed class WorkflowIdentifier {
55+
56+
/**
57+
* The fully-qualified name of the type of workflow this identifier identifies. Computed lazily
58+
* and cached.
59+
*/
60+
protected abstract val typeName: String
61+
62+
/**
63+
* An optional identifier from [ImpostorWorkflow.realIdentifier] that will be used to further
64+
* narrow the scope of this identifier.
65+
*/
66+
protected abstract val proxiedIdentifier: WorkflowIdentifier?
4867

4968
/**
50-
* The fully-qualified name of [type]. Computed lazily.
69+
* If this identifier is snapshottable, returns the serialized form of the identifier.
70+
* If it is not snapshottable. returns null.
5171
*/
52-
private val typeString: String by lazy(PUBLICATION) { type.java.name }
72+
abstract fun maybeToByteString(): ByteString?
5373

5474
/**
5575
* Returns a description of this identifier including the name of its workflow type and any
56-
* [proxiedIdentifier]s. Computes [typeString] if it has not already been computed.
76+
* [ImpostorWorkflow.realIdentifier]s.
5777
*/
5878
override fun toString(): String =
5979
generateSequence(this) { it.proxiedIdentifier }
60-
.joinToString { it.typeString }
80+
.joinToString { it.typeName }
6181
.let { "WorkflowIdentifier($it)" }
6282

83+
companion object {
84+
fun parse(bytes: ByteString): WorkflowIdentifier? = WorkflowClassIdentifier.parse(bytes)
85+
}
86+
}
87+
88+
/**
89+
* @param type The [KClass] of the [Workflow] this identifier identifies.
90+
*/
91+
@OptIn(ExperimentalWorkflowApi::class)
92+
private class WorkflowClassIdentifier(
93+
private val type: KClass<out Workflow<*, *, *>>,
94+
override val proxiedIdentifier: WorkflowIdentifier?
95+
) : WorkflowIdentifier() {
96+
6397
/**
64-
* Serializes this identifier to the sink. It can be read back with [WorkflowIdentifier.read].
98+
* The fully-qualified name of the type of workflow this identifier identifies.
99+
* Note that Class.name is already cached, so we don't need to cache it ourselves.
65100
*/
66-
fun write(sink: BufferedSink) {
67-
sink.writeUtf8WithLength(typeString)
68-
if (proxiedIdentifier != null) {
69-
sink.writeByte(PROXY_IDENTIFIER_TAG.toInt())
70-
proxiedIdentifier.write(sink)
71-
} else {
72-
sink.writeByte(NO_PROXY_IDENTIFIER_TAG.toInt())
101+
override val typeName: String get() = type.java.name
102+
103+
/**
104+
* Serializes this identifier to a [ByteString] if its [proxiedIdentifier] is serializable, else
105+
* returns null. The non-null [ByteString] can be read back with [WorkflowIdentifier.read].
106+
*/
107+
override fun maybeToByteString(): ByteString? {
108+
val proxiedBytes = proxiedIdentifier?.let {
109+
// If we have a proxied identifier but it's not serializable, then we can't be serializable
110+
// either.
111+
it.maybeToByteString() ?: return null
112+
}
113+
114+
return Buffer().let { sink ->
115+
sink.writeUtf8WithLength(typeName)
116+
if (proxiedBytes != null) {
117+
sink.writeByte(PROXY_IDENTIFIER_TAG.toInt())
118+
sink.write(proxiedBytes)
119+
} else {
120+
sink.writeByte(NO_PROXY_IDENTIFIER_TAG.toInt())
121+
}
122+
sink.readByteString()
73123
}
74124
}
75125

76126
/**
77-
* Determines equality to another [WorkflowIdentifier] by comparing their [type]s and their
127+
* Determines equality to another [WorkflowClassIdentifier] by comparing their [type]s and their
78128
* [proxiedIdentifier]s.
79129
*/
80130
override fun equals(other: Any?): Boolean = when {
81131
this === other -> true
82-
other !is WorkflowIdentifier -> false
132+
other !is WorkflowClassIdentifier -> false
83133
else -> type == other.type && proxiedIdentifier == other.proxiedIdentifier
84134
}
85135

@@ -103,31 +153,68 @@ class WorkflowIdentifier internal constructor(
103153
* @throws ClassNotFoundException if one of the workflow types can't be found in the class
104154
* loader
105155
*/
106-
fun read(source: BufferedSource): WorkflowIdentifier? {
156+
fun parse(bytes: ByteString): WorkflowIdentifier? = Buffer().let { source ->
157+
source.write(bytes)
158+
107159
try {
108160
val typeString = source.readUtf8WithLength()
109161
val proxiedIdentifier = when (source.readByte()) {
110162
NO_PROXY_IDENTIFIER_TAG -> null
111-
PROXY_IDENTIFIER_TAG -> read(source)
163+
PROXY_IDENTIFIER_TAG -> parse(source.readByteString())
112164
else -> throw IllegalArgumentException("Invalid WorkflowIdentifier")
113165
}
114166

115167
@Suppress("UNCHECKED_CAST")
116168
val type = Class.forName(typeString) as Class<out Workflow<Nothing, Any, Any>>
117-
return WorkflowIdentifier(type.kotlin, proxiedIdentifier)
169+
return WorkflowClassIdentifier(type.kotlin, proxiedIdentifier)
118170
} catch (e: EOFException) {
119171
throw IllegalArgumentException("Invalid WorkflowIdentifier")
120172
}
121173
}
122174
}
123175
}
124176

177+
/**
178+
* A [WorkflowIdentifier] that
179+
*/
180+
@OptIn(ExperimentalWorkflowApi::class)
181+
private class TransientWorkflowIdentifier(private val type: KType) : WorkflowIdentifier() {
182+
override val proxiedIdentifier: WorkflowIdentifier? get() = null
183+
override val typeName: String by lazy(PUBLICATION) { type.toString() }
184+
override fun maybeToByteString(): ByteString? = null
185+
186+
/** Determines equality to another [TransientWorkflowIdentifier] by comparing their [type]s. */
187+
override fun equals(other: Any?): Boolean = when {
188+
this === other -> true
189+
other !is TransientWorkflowIdentifier -> false
190+
else -> type == other.type
191+
}
192+
193+
/** Derives a hashcode from [type]. */
194+
override fun hashCode(): Int = type.hashCode()
195+
}
196+
197+
/**
198+
* Creates a [WorkflowIdentifier] that is not capable of being snapshotted and will cause any
199+
* [ImpostorWorkflow] workflow identified by it to also not be snapshotted.
200+
*
201+
* **This function should not be used for [ImpostorWorkflow]s that wrap arbitrary workflows**, since
202+
* those workflows may expect to be on snapshotted. Using such identifiers _anywhere in the
203+
* [ImpostorWorkflow.realIdentifier] chain_ will disable snapshotting for that workflow. **This
204+
* function should only be used for [ImpostorWorkflow]s that wrap a closed set of known workflow
205+
* types.**
206+
*/
207+
@ExperimentalWorkflowApi
208+
@Suppress("unused")
209+
fun unsnapshottableIdentifier(type: KType): WorkflowIdentifier =
210+
TransientWorkflowIdentifier(type)
211+
125212
/**
126213
* The [WorkflowIdentifier] that identifies this [Workflow].
127214
*/
128215
@ExperimentalWorkflowApi
129216
val Workflow<*, *, *>.identifier: WorkflowIdentifier
130217
get() {
131218
val proxiedIdentifier = (this as? ImpostorWorkflow)?.realIdentifier
132-
return WorkflowIdentifier(type = this::class, proxiedIdentifier = proxiedIdentifier)
219+
return WorkflowClassIdentifier(type = this::class, proxiedIdentifier = proxiedIdentifier)
133220
}

0 commit comments

Comments
 (0)