1
- use futures_util:: { SinkExt , StreamExt } ;
1
+ //! An example of ingesting messages from a PubSub subscription, applying a
2
+ //! transformation, then submitting those transformations to another PubSub topic.
3
+
4
+ use futures_util:: { SinkExt , StreamExt , TryFutureExt } ;
2
5
use hedwig:: {
3
6
googlepubsub:: {
4
- AuthFlow , ClientBuilder , ClientBuilderConfig , PubSubConfig , ServiceAccountAuth ,
5
- StreamSubscriptionConfig , SubscriptionConfig , SubscriptionName , TopicConfig , TopicName ,
7
+ AcknowledgeToken , AuthFlow , ClientBuilder , ClientBuilderConfig , PubSubConfig ,
8
+ PubSubMessage , PublishError , ServiceAccountAuth , StreamSubscriptionConfig ,
9
+ SubscriptionConfig , SubscriptionName , TopicConfig , TopicName ,
6
10
} ,
7
11
validators, Consumer , DecodableMessage , EncodableMessage , Headers , Publisher ,
8
12
} ;
9
- use std:: time:: SystemTime ;
13
+ use std:: { error :: Error as StdError , time:: SystemTime } ;
10
14
use structopt:: StructOpt ;
11
15
12
- #[ derive( Clone , PartialEq , Eq , prost:: Message ) ]
16
+ const USER_CREATED_TOPIC : & str = "user.created" ;
17
+ const USER_UPDATED_TOPIC : & str = "user.updated" ;
18
+
19
+ /// The input data, representing some user being created with the given name
20
+ #[ derive( PartialEq , Eq , prost:: Message ) ]
13
21
struct UserCreatedMessage {
14
22
#[ prost( string, tag = "1" ) ]
15
- payload : String ,
23
+ name : String ,
16
24
}
17
25
18
- impl < ' a > EncodableMessage for UserCreatedMessage {
26
+ impl EncodableMessage for UserCreatedMessage {
19
27
type Error = validators:: ProstValidatorError ;
20
28
type Validator = validators:: ProstValidator ;
21
29
fn topic ( & self ) -> hedwig:: Topic {
22
- "user.created" . into ( )
30
+ USER_CREATED_TOPIC . into ( )
23
31
}
24
- fn encode ( self , validator : & Self :: Validator ) -> Result < hedwig:: ValidatedMessage , Self :: Error > {
32
+ fn encode ( & self , validator : & Self :: Validator ) -> Result < hedwig:: ValidatedMessage , Self :: Error > {
25
33
Ok ( validator. validate (
26
34
uuid:: Uuid :: new_v4 ( ) ,
27
35
SystemTime :: now ( ) ,
28
36
"user.created/1.0" ,
29
37
Headers :: new ( ) ,
30
- & self ,
38
+ self ,
31
39
) ?)
32
40
}
33
41
}
@@ -42,6 +50,46 @@ impl DecodableMessage for UserCreatedMessage {
42
50
}
43
51
}
44
52
53
+ /// The output data, where the given user has now been assigned an ID and some metadata
54
+ #[ derive( PartialEq , Eq , prost:: Message ) ]
55
+ struct UserUpdatedMessage {
56
+ #[ prost( string, tag = "1" ) ]
57
+ name : String ,
58
+
59
+ #[ prost( int64, tag = "2" ) ]
60
+ id : i64 ,
61
+
62
+ #[ prost( string, tag = "3" ) ]
63
+ metadata : String ,
64
+ }
65
+
66
+ #[ derive( Debug ) ]
67
+ struct TransformedMessage {
68
+ // keep the input's ack token to ack when the output is successfully published, or nack on
69
+ // failure
70
+ input_token : AcknowledgeToken ,
71
+ output : UserUpdatedMessage ,
72
+ }
73
+
74
+ impl EncodableMessage for TransformedMessage {
75
+ type Error = validators:: ProstValidatorError ;
76
+ type Validator = validators:: ProstValidator ;
77
+
78
+ fn topic ( & self ) -> hedwig:: Topic {
79
+ USER_UPDATED_TOPIC . into ( )
80
+ }
81
+
82
+ fn encode ( & self , validator : & Self :: Validator ) -> Result < hedwig:: ValidatedMessage , Self :: Error > {
83
+ Ok ( validator. validate (
84
+ uuid:: Uuid :: new_v4 ( ) ,
85
+ SystemTime :: now ( ) ,
86
+ "user.updated/1.0" ,
87
+ Headers :: new ( ) ,
88
+ & self . output ,
89
+ ) ?)
90
+ }
91
+ }
92
+
45
93
#[ derive( Debug , StructOpt ) ]
46
94
struct Args {
47
95
/// The name of the pubsub project
@@ -50,7 +98,7 @@ struct Args {
50
98
}
51
99
52
100
#[ tokio:: main( flavor = "current_thread" ) ]
53
- async fn main ( ) -> Result < ( ) , Box < dyn std :: error :: Error + ' static > > {
101
+ async fn main ( ) -> Result < ( ) , Box < dyn StdError > > {
54
102
let args = Args :: from_args ( ) ;
55
103
56
104
println ! ( "Building PubSub clients" ) ;
@@ -61,49 +109,59 @@ async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
61
109
)
62
110
. await ?;
63
111
64
- let topic_name = TopicName :: new ( "pubsub_example_topic" ) ;
65
- let subscription_name = SubscriptionName :: new ( "pubsub_example_subscription" ) ;
112
+ let input_topic_name = TopicName :: new ( USER_CREATED_TOPIC ) ;
113
+ let subscription_name = SubscriptionName :: new ( "user-metadata-updaters" ) ;
114
+
115
+ let output_topic_name = TopicName :: new ( USER_UPDATED_TOPIC ) ;
116
+ const APP_NAME : & str = "user-metadata-updater" ;
66
117
67
118
let mut publisher_client = builder
68
- . build_publisher ( & args. project_name , "myapp_publisher" )
69
- . await ?;
70
- let mut consumer_client = builder
71
- . build_consumer ( & args. project_name , "myapp_consumer" )
119
+ . build_publisher ( & args. project_name , APP_NAME )
72
120
. await ?;
121
+ let mut consumer_client = builder. build_consumer ( & args. project_name , APP_NAME ) . await ?;
73
122
74
- println ! ( "Creating topic {:?}" , & topic_name) ;
123
+ for topic_name in [ & input_topic_name, & output_topic_name] {
124
+ println ! ( "Creating topic {:?}" , topic_name) ;
75
125
76
- publisher_client
77
- . create_topic ( TopicConfig {
78
- name : topic_name. clone ( ) ,
79
- ..TopicConfig :: default ( )
80
- } )
81
- . await ?;
126
+ publisher_client
127
+ . create_topic ( TopicConfig {
128
+ name : topic_name. clone ( ) ,
129
+ ..TopicConfig :: default ( )
130
+ } )
131
+ . await ?;
132
+ }
82
133
83
134
println ! ( "Creating subscription {:?}" , & subscription_name) ;
84
135
85
136
consumer_client
86
137
. create_subscription ( SubscriptionConfig {
87
- topic : topic_name . clone ( ) ,
138
+ topic : input_topic_name . clone ( ) ,
88
139
name : subscription_name. clone ( ) ,
89
140
..SubscriptionConfig :: default ( )
90
141
} )
91
142
. await ?;
92
143
93
- println ! ( "Publishing message to topic" ) ;
144
+ println ! (
145
+ "Synthesizing input messages for topic {:?}" ,
146
+ & input_topic_name
147
+ ) ;
94
148
95
- let validator = hedwig:: validators:: ProstValidator :: new ( ) ;
96
- let mut publisher = publisher_client. publisher ( ) . publish_sink ( validator) ;
149
+ {
150
+ let validator = validators:: ProstValidator :: new ( ) ;
151
+ let mut input_sink =
152
+ Publisher :: < UserCreatedMessage > :: publish_sink ( publisher_client. publisher ( ) , validator) ;
97
153
98
- for i in 1 ..=10 {
99
- let message = UserCreatedMessage {
100
- payload : format ! ( "this is message #{}" , i) ,
101
- } ;
154
+ for i in 1 ..=10 {
155
+ let message = UserCreatedMessage {
156
+ name : format ! ( "Example Name #{}" , i) ,
157
+ } ;
102
158
103
- publisher. send ( message) . await ?;
159
+ input_sink. feed ( message) . await ?;
160
+ }
161
+ input_sink. flush ( ) . await ?;
104
162
}
105
163
106
- println ! ( "Reading back published message " ) ;
164
+ println ! ( "Ingesting input messages, applying transformations, and publishing to destination " ) ;
107
165
108
166
let mut read_stream = consumer_client
109
167
. stream_subscription (
@@ -114,35 +172,70 @@ async fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
114
172
hedwig:: validators:: prost:: ExactSchemaMatcher :: new ( "user.created/1.0" ) ,
115
173
) ) ;
116
174
175
+ let mut output_sink = Publisher :: < TransformedMessage , _ > :: publish_sink_with_responses (
176
+ publisher_client. publisher ( ) ,
177
+ validators:: ProstValidator :: new ( ) ,
178
+ futures_util:: sink:: unfold ( ( ) , |_, message : TransformedMessage | async move {
179
+ // if the output is successfully sent, ack the input to mark it as processed
180
+ message. input_token . ack ( ) . await
181
+ } ) ,
182
+ ) ;
183
+
117
184
for i in 1 ..=10 {
118
- let message = read_stream
185
+ let PubSubMessage { ack_token , message } = read_stream
119
186
. next ( )
120
187
. await
121
- . ok_or ( "unexpected end of stream" ) ??
122
- . ack ( )
123
- . await ?;
188
+ . expect ( "stream should have 10 elements" ) ?;
124
189
125
- assert_eq ! (
126
- message,
127
- UserCreatedMessage {
128
- payload: format!( "this is message #{}" , i)
129
- }
130
- ) ;
190
+ assert_eq ! ( & message. name, & format!( "Example Name #{}" , i) ) ;
191
+
192
+ let transformed = TransformedMessage {
193
+ output : UserUpdatedMessage {
194
+ name : message. name ,
195
+ id : random_id ( ) ,
196
+ metadata : "some metadata" . into ( ) ,
197
+ } ,
198
+ input_token : ack_token,
199
+ } ;
200
+
201
+ output_sink
202
+ . feed ( transformed)
203
+ . or_else ( |publish_error| async move {
204
+ // if publishing fails, nack the failed messages to allow later retries
205
+ Err ( match publish_error {
206
+ PublishError :: Publish { cause, messages } => {
207
+ for failed_transform in messages {
208
+ failed_transform. input_token . nack ( ) . await ?;
209
+ }
210
+ Box :: < dyn StdError > :: from ( cause)
211
+ }
212
+ err => Box :: < dyn StdError > :: from ( err) ,
213
+ } )
214
+ } )
215
+ . await ?
131
216
}
217
+ output_sink. flush ( ) . await ?;
132
218
133
- println ! ( "All messages matched!" ) ;
219
+ println ! ( "All messages matched and published successfully !" ) ;
134
220
135
221
println ! ( "Deleting subscription {:?}" , & subscription_name) ;
136
222
137
223
consumer_client
138
224
. delete_subscription ( subscription_name)
139
225
. await ?;
140
226
141
- println ! ( "Deleting topic {:?}" , & topic_name) ;
227
+ for topic_name in [ input_topic_name, output_topic_name] {
228
+ println ! ( "Deleting topic {:?}" , & topic_name) ;
142
229
143
- publisher_client. delete_topic ( topic_name) . await ?;
230
+ publisher_client. delete_topic ( topic_name) . await ?;
231
+ }
144
232
145
233
println ! ( "Done" ) ;
146
234
147
235
Ok ( ( ) )
148
236
}
237
+
238
+ fn random_id ( ) -> i64 {
239
+ 4 // chosen by fair dice roll.
240
+ // guaranteed to be random.
241
+ }
0 commit comments