embulk-input-pubsub 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.scalafmt.conf +13 -0
  4. data/LICENSE +21 -0
  5. data/README.md +75 -0
  6. data/build.gradle +87 -0
  7. data/classpath/animal-sniffer-annotations-1.18.jar +0 -0
  8. data/classpath/annotations-4.1.1.4.jar +0 -0
  9. data/classpath/auto-value-annotations-1.6.6.jar +0 -0
  10. data/classpath/checker-compat-qual-2.5.5.jar +0 -0
  11. data/classpath/commons-codec-1.11.jar +0 -0
  12. data/classpath/commons-lang3-3.5.jar +0 -0
  13. data/classpath/commons-logging-1.2.jar +0 -0
  14. data/classpath/embulk-input-pubsub-0.0.1-shadow.jar +0 -0
  15. data/classpath/error_prone_annotations-2.3.2.jar +0 -0
  16. data/classpath/google-auth-library-credentials-0.18.0.jar +0 -0
  17. data/classpath/google-auth-library-oauth2-http-0.18.0.jar +0 -0
  18. data/classpath/google-cloud-core-1.91.3.jar +0 -0
  19. data/classpath/google-cloud-core-grpc-1.91.3.jar +0 -0
  20. data/classpath/google-http-client-1.32.1.jar +0 -0
  21. data/classpath/google-http-client-jackson2-1.32.1.jar +0 -0
  22. data/classpath/grpc-alts-1.23.0.jar +0 -0
  23. data/classpath/grpc-auth-1.23.0.jar +0 -0
  24. data/classpath/grpc-context-1.24.1.jar +0 -0
  25. data/classpath/grpc-google-cloud-pubsub-v1-1.82.0.jar +0 -0
  26. data/classpath/grpc-grpclb-1.23.0.jar +0 -0
  27. data/classpath/grpc-protobuf-1.24.1.jar +0 -0
  28. data/classpath/grpc-protobuf-lite-1.24.1.jar +0 -0
  29. data/classpath/gson-2.8.5.jar +0 -0
  30. data/classpath/httpclient-4.5.10.jar +0 -0
  31. data/classpath/httpcore-4.4.12.jar +0 -0
  32. data/classpath/j2objc-annotations-1.3.jar +0 -0
  33. data/classpath/jackson-core-2.9.9.jar +0 -0
  34. data/classpath/javax.annotation-api-1.3.2.jar +0 -0
  35. data/classpath/jsr305-3.0.2.jar +0 -0
  36. data/classpath/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar +0 -0
  37. data/classpath/opencensus-api-0.24.0.jar +0 -0
  38. data/classpath/opencensus-contrib-grpc-metrics-0.21.0.jar +0 -0
  39. data/classpath/opencensus-contrib-http-util-0.24.0.jar +0 -0
  40. data/classpath/perfmark-api-0.17.0.jar +0 -0
  41. data/classpath/proto-google-cloud-pubsub-v1-1.82.0.jar +0 -0
  42. data/classpath/proto-google-common-protos-1.17.0.jar +0 -0
  43. data/classpath/proto-google-iam-v1-0.13.0.jar +0 -0
  44. data/classpath/protobuf-java-3.10.0.jar +0 -0
  45. data/classpath/protobuf-java-util-3.10.0.jar +0 -0
  46. data/classpath/scala-library-2.13.1.jar +0 -0
  47. data/classpath/threetenbp-1.3.3.jar +0 -0
  48. data/examples/pubsub2stdout.yaml +10 -0
  49. data/gradle.properties +1 -0
  50. data/gradle/wrapper/gradle-wrapper.jar +0 -0
  51. data/gradle/wrapper/gradle-wrapper.properties +6 -0
  52. data/gradlew +172 -0
  53. data/gradlew.bat +84 -0
  54. data/lib/embulk/input/pubsub.rb +3 -0
  55. data/src/main/java/com/embulk/input/pubsub/checkpoint/Checkpoint.java +734 -0
  56. data/src/main/java/com/embulk/input/pubsub/checkpoint/CheckpointOrBuilder.java +33 -0
  57. data/src/main/java/com/embulk/input/pubsub/checkpoint/CheckpointProtos.java +61 -0
  58. data/src/main/resources/checkpoint.proto +11 -0
  59. data/src/main/scala/org/embulk/input/pubsub/PluginTask.scala +42 -0
  60. data/src/main/scala/org/embulk/input/pubsub/PubsubBatchSubscriber.scala +103 -0
  61. data/src/main/scala/org/embulk/input/pubsub/PubsubInputPlugin.scala +142 -0
  62. data/src/main/scala/org/embulk/input/pubsub/checkpoint/StoredCheckpoint.scala +123 -0
  63. metadata +105 -0
@@ -0,0 +1,33 @@
1
+ // Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ // source: src/main/resources/checkpoint.proto
3
+
4
+ package com.embulk.input.pubsub.checkpoint;
5
+
6
+ public interface CheckpointOrBuilder extends
7
+ // @@protoc_insertion_point(interface_extends:Checkpoint)
8
+ com.google.protobuf.MessageOrBuilder {
9
+
10
+ /**
11
+ * <code>repeated .google.pubsub.v1.PubsubMessage messages = 1;</code>
12
+ */
13
+ java.util.List<com.google.pubsub.v1.PubsubMessage>
14
+ getMessagesList();
15
+ /**
16
+ * <code>repeated .google.pubsub.v1.PubsubMessage messages = 1;</code>
17
+ */
18
+ com.google.pubsub.v1.PubsubMessage getMessages(int index);
19
+ /**
20
+ * <code>repeated .google.pubsub.v1.PubsubMessage messages = 1;</code>
21
+ */
22
+ int getMessagesCount();
23
+ /**
24
+ * <code>repeated .google.pubsub.v1.PubsubMessage messages = 1;</code>
25
+ */
26
+ java.util.List<? extends com.google.pubsub.v1.PubsubMessageOrBuilder>
27
+ getMessagesOrBuilderList();
28
+ /**
29
+ * <code>repeated .google.pubsub.v1.PubsubMessage messages = 1;</code>
30
+ */
31
+ com.google.pubsub.v1.PubsubMessageOrBuilder getMessagesOrBuilder(
32
+ int index);
33
+ }
@@ -0,0 +1,61 @@
1
+ // Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ // source: src/main/resources/checkpoint.proto
3
+
4
+ package com.embulk.input.pubsub.checkpoint;
5
+
6
+ public final class CheckpointProtos {
7
+ private CheckpointProtos() {}
8
+ public static void registerAllExtensions(
9
+ com.google.protobuf.ExtensionRegistryLite registry) {
10
+ }
11
+
12
+ public static void registerAllExtensions(
13
+ com.google.protobuf.ExtensionRegistry registry) {
14
+ registerAllExtensions(
15
+ (com.google.protobuf.ExtensionRegistryLite) registry);
16
+ }
17
+ static final com.google.protobuf.Descriptors.Descriptor
18
+ internal_static_Checkpoint_descriptor;
19
+ static final
20
+ com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
21
+ internal_static_Checkpoint_fieldAccessorTable;
22
+
23
+ public static com.google.protobuf.Descriptors.FileDescriptor
24
+ getDescriptor() {
25
+ return descriptor;
26
+ }
27
+ private static com.google.protobuf.Descriptors.FileDescriptor
28
+ descriptor;
29
+ static {
30
+ java.lang.String[] descriptorData = {
31
+ "\n#src/main/resources/checkpoint.proto\032(g" +
32
+ "oogleapis/google/pubsub/v1/pubsub.proto\"" +
33
+ "?\n\nCheckpoint\0221\n\010messages\030\001 \003(\0132\037.google" +
34
+ ".pubsub.v1.PubsubMessageB8\n\"com.embulk.i" +
35
+ "nput.pubsub.checkpointB\020CheckpointProtos" +
36
+ "P\001b\006proto3"
37
+ };
38
+ com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
39
+ new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() {
40
+ public com.google.protobuf.ExtensionRegistry assignDescriptors(
41
+ com.google.protobuf.Descriptors.FileDescriptor root) {
42
+ descriptor = root;
43
+ return null;
44
+ }
45
+ };
46
+ com.google.protobuf.Descriptors.FileDescriptor
47
+ .internalBuildGeneratedFileFrom(descriptorData,
48
+ new com.google.protobuf.Descriptors.FileDescriptor[] {
49
+ com.google.pubsub.v1.PubsubProto.getDescriptor(),
50
+ }, assigner);
51
+ internal_static_Checkpoint_descriptor =
52
+ getDescriptor().getMessageTypes().get(0);
53
+ internal_static_Checkpoint_fieldAccessorTable = new
54
+ com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
55
+ internal_static_Checkpoint_descriptor,
56
+ new java.lang.String[] { "Messages", });
57
+ com.google.pubsub.v1.PubsubProto.getDescriptor();
58
+ }
59
+
60
+ // @@protoc_insertion_point(outer_class_scope)
61
+ }
@@ -0,0 +1,11 @@
1
+ syntax = "proto3";
2
+
3
+ option java_multiple_files = true;
4
+ option java_outer_classname = "CheckpointProtos";
5
+ option java_package = "com.embulk.input.pubsub.checkpoint";
6
+
7
+ import "googleapis/google/pubsub/v1/pubsub.proto";
8
+
9
+ message Checkpoint {
10
+ repeated google.pubsub.v1.PubsubMessage messages = 1;
11
+ }
@@ -0,0 +1,42 @@
1
+ package org.embulk.input.pubsub
2
+
3
+ import java.util.Optional
4
+
5
+ import org.embulk.config.{Config, ConfigDefault, ConfigInject, Task}
6
+ import org.embulk.spi.BufferAllocator
7
+
8
+ trait PluginTask extends Task {
9
+
10
+ @Config("project_id")
11
+ def getProjectId: String
12
+
13
+ @Config("subscription_id")
14
+ def getSubscriptionId: String
15
+
16
+ @Config("json_keyfile")
17
+ def getJsonKeyfile: String
18
+
19
+ @Config("num_tasks")
20
+ @ConfigDefault("1")
21
+ def getNumTasks: Int
22
+
23
+ @Config("max_messages")
24
+ @ConfigDefault("10")
25
+ def getMaxMessages: Int
26
+
27
+ @Config("checkpoint_basedir")
28
+ @ConfigDefault("null")
29
+ def getCheckpointBasedir: Optional[String]
30
+
31
+ @Config("checkpoint")
32
+ @ConfigDefault("null")
33
+ def getCheckpoint: Optional[String]
34
+ def setCheckpoint(checkpoint: Optional[String]): Unit
35
+
36
+ @Config("payload_encoding")
37
+ @ConfigDefault("\"string\"")
38
+ def getPayloadEncoding: String
39
+
40
+ @ConfigInject
41
+ def getBufferAllocator: BufferAllocator
42
+ }
@@ -0,0 +1,103 @@
1
+ package org.embulk.input.pubsub
2
+
3
+ import java.io.FileInputStream
4
+
5
+ import com.google.api.gax.core.FixedCredentialsProvider
6
+ import com.google.auth.oauth2.GoogleCredentials
7
+ import com.google.cloud.pubsub.v1.stub.{
8
+ GrpcSubscriberStub,
9
+ SubscriberStubSettings
10
+ }
11
+ import com.google.protobuf.Empty
12
+ import com.google.pubsub.v1.{
13
+ AcknowledgeRequest,
14
+ ProjectSubscriptionName,
15
+ PullRequest,
16
+ PullResponse
17
+ }
18
+ import org.embulk.input.pubsub.checkpoint.StoredCheckpoint
19
+
20
+ import scala.jdk.CollectionConverters._
21
+ import scala.util.{Success, Try}
22
+
23
+ /**
24
+ * A subscriber for Cloud Pub/Sub calls batch based pulls with checkpoint.
25
+ *
26
+ * @param projectId
27
+ * @param subscriptionName
28
+ * @param pathToCredJson
29
+ */
30
+ case class PubsubBatchSubscriber private (
31
+ projectId: String,
32
+ subscriptionName: String,
33
+ pathToCredJson: String
34
+ ) {
35
+ private val credentials =
36
+ GoogleCredentials.fromStream(new FileInputStream(pathToCredJson))
37
+ private val settings = SubscriberStubSettings
38
+ .newBuilder()
39
+ .setCredentialsProvider(FixedCredentialsProvider.create(credentials))
40
+ .setTransportChannelProvider(
41
+ SubscriberStubSettings.defaultGrpcTransportProviderBuilder().build()
42
+ )
43
+ .build()
44
+
45
+ def pull(count: Int, checkpointDir: Option[String]): Try[StoredCheckpoint] = {
46
+ val subscription =
47
+ ProjectSubscriptionName.of(projectId, subscriptionName).toString
48
+ val subscriber = GrpcSubscriberStub.create(settings)
49
+
50
+ for {
51
+ res <- pullImpl(subscriber, subscription, count)
52
+ messages = res.getReceivedMessagesList.asScala
53
+ checkpoint <- StoredCheckpoint.create(
54
+ messages.map(_.getMessage).toSeq,
55
+ checkpointDir
56
+ )
57
+ _ <- ackImpl(subscriber, subscription, messages.map(_.getAckId))
58
+ } yield checkpoint
59
+ }
60
+
61
+ private def pullImpl(
62
+ subscriber: GrpcSubscriberStub,
63
+ subscription: String,
64
+ count: Int
65
+ ): Try[PullResponse] = {
66
+ val req = PullRequest
67
+ .newBuilder()
68
+ .setSubscription(subscription)
69
+ .setReturnImmediately(true)
70
+ .setMaxMessages(count)
71
+ .build()
72
+
73
+ Try(subscriber.pullCallable().call(req))
74
+ }
75
+
76
+ private def ackImpl(
77
+ subscriber: GrpcSubscriberStub,
78
+ subscription: String,
79
+ ackIds: Iterable[String]
80
+ ): Try[Empty] = {
81
+ if (ackIds.nonEmpty) {
82
+ val ack = AcknowledgeRequest
83
+ .newBuilder()
84
+ .setSubscription(subscription)
85
+ .addAllAckIds(ackIds.asJava)
86
+ .build()
87
+
88
+ Try(subscriber.acknowledgeCallable().call(ack))
89
+ } else {
90
+ Success(Empty.getDefaultInstance)
91
+ }
92
+ }
93
+
94
+ }
95
+
96
+ object PubsubBatchSubscriber {
97
+ def of(task: PluginTask): PubsubBatchSubscriber =
98
+ PubsubBatchSubscriber(
99
+ task.getProjectId,
100
+ task.getSubscriptionId,
101
+ task.getJsonKeyfile
102
+ )
103
+ }
@@ -0,0 +1,142 @@
1
+ package org.embulk.input.pubsub
2
+
3
+ import java.nio.charset.StandardCharsets
4
+ import java.util.{Base64, Optional, List => JList}
5
+
6
+ import com.fasterxml.jackson.databind.ObjectMapper
7
+ import org.embulk.config.{
8
+ ConfigDiff,
9
+ ConfigException,
10
+ ConfigSource,
11
+ TaskReport,
12
+ TaskSource
13
+ }
14
+ import org.embulk.input.pubsub.checkpoint.StoredCheckpoint
15
+ import org.embulk.spi.`type`.Types
16
+ import org.embulk.spi.{
17
+ DataException,
18
+ Exec,
19
+ InputPlugin,
20
+ PageBuilder,
21
+ PageOutput,
22
+ Schema
23
+ }
24
+ import org.embulk.spi.json.JsonParser
25
+ import org.slf4j.LoggerFactory
26
+
27
+ import scala.jdk.OptionConverters._
28
+ import scala.jdk.CollectionConverters._
29
+ import scala.util.{Failure, Success}
30
+
31
+ case class PubsubInputPlugin() extends InputPlugin {
32
+ private val logger = LoggerFactory.getLogger(this.getClass)
33
+
34
+ private val jsonParser = new JsonParser()
35
+ private val objectMapper = new ObjectMapper()
36
+
37
+ private val schema = Schema
38
+ .builder()
39
+ .add("payload", Types.STRING) // string or base64 encoded bytes
40
+ .add("attribute", Types.JSON)
41
+ .build()
42
+
43
+ override def transaction(
44
+ config: ConfigSource,
45
+ control: InputPlugin.Control
46
+ ): ConfigDiff = {
47
+ val task = config.loadConfig(classOf[PluginTask])
48
+
49
+ if (!task.getCheckpoint.isPresent) {
50
+ val sub = PubsubBatchSubscriber.of(task)
51
+ val checkpoint =
52
+ sub.pull(task.getMaxMessages, task.getCheckpointBasedir.toScala).get
53
+ task.setCheckpoint(Optional.of(checkpoint.id))
54
+
55
+ logger.info(s"Created a new checkpoint! : ${checkpoint.id}")
56
+ }
57
+
58
+ resume(task.dump(), schema, task.getNumTasks, control)
59
+ }
60
+
61
+ override def resume(
62
+ taskSource: TaskSource,
63
+ schema: Schema,
64
+ taskCount: Int,
65
+ control: InputPlugin.Control
66
+ ): ConfigDiff = {
67
+ control.run(taskSource, schema, taskCount)
68
+ Exec.newConfigDiff()
69
+ }
70
+
71
+ override def cleanup(
72
+ taskSource: TaskSource,
73
+ schema: Schema,
74
+ taskCount: Int,
75
+ successTaskReports: JList[TaskReport]
76
+ ): Unit = {
77
+ val task = taskSource.loadTask(classOf[PluginTask])
78
+
79
+ val checkpointId = task.getCheckpoint.get()
80
+ val checkpoint =
81
+ StoredCheckpoint.from(checkpointId, task.getCheckpointBasedir.isPresent)
82
+ checkpoint match {
83
+ case Success(sc) =>
84
+ sc.cleanup match {
85
+ case Success(_) =>
86
+ case Failure(e) => logger.error(s"failed to cleanup: ${e.toString}")
87
+ }
88
+ case Failure(e) =>
89
+ logger.error(s"failed to fetch checkpoint: ${e.toString}")
90
+ }
91
+ }
92
+
93
+ override def run(
94
+ taskSource: TaskSource,
95
+ schema: Schema,
96
+ taskIndex: Int,
97
+ output: PageOutput
98
+ ): TaskReport = {
99
+ val task = taskSource.loadTask(classOf[PluginTask])
100
+ val allocator = task.getBufferAllocator
101
+ val pageBuilder = new PageBuilder(allocator, schema, output)
102
+
103
+ val encoder = task.getPayloadEncoding match {
104
+ case "string" =>
105
+ (data: Array[Byte]) => new String(data, StandardCharsets.UTF_8)
106
+ case "binary" =>
107
+ (data: Array[Byte]) => Base64.getEncoder.encodeToString(data)
108
+ case e => throw new ConfigException(s"unsupported encoding: ${e}")
109
+ }
110
+
111
+ val checkpointId = task.getCheckpoint.get()
112
+ val checkpoint =
113
+ StoredCheckpoint.from(checkpointId, task.getCheckpointBasedir.isPresent)
114
+ val messages = checkpoint match {
115
+ case Success(cp) => cp.content.getMessagesList.asScala
116
+ case _ =>
117
+ throw new DataException(s"unexpected checkpoint state: ${checkpoint}")
118
+ }
119
+
120
+ messages.foreach { msg =>
121
+ pageBuilder.setString(
122
+ pageBuilder.getSchema.getColumn(0),
123
+ encoder(msg.getData.toByteArray)
124
+ )
125
+
126
+ val json = objectMapper.writeValueAsString(msg.getAttributesMap)
127
+ pageBuilder.setJson(
128
+ pageBuilder.getSchema.getColumn(1),
129
+ jsonParser.parse(json)
130
+ )
131
+
132
+ pageBuilder.addRecord()
133
+ }
134
+ pageBuilder.finish()
135
+
136
+ Exec.newTaskReport()
137
+ }
138
+
139
+ override def guess(config: ConfigSource): ConfigDiff =
140
+ Exec.newConfigDiff()
141
+
142
+ }
@@ -0,0 +1,123 @@
1
+ package org.embulk.input.pubsub.checkpoint
2
+
3
+ import java.io.{File, FileInputStream, FileOutputStream}
4
+
5
+ import com.embulk.input.pubsub.checkpoint.Checkpoint
6
+ import com.google.pubsub.v1.PubsubMessage
7
+ import org.embulk.config.ConfigException
8
+
9
+ import scala.collection.mutable
10
+ import scala.util.{Failure, Success, Try}
11
+ import scala.jdk.CollectionConverters._
12
+
13
+ /**
14
+ * A checkpoint stored in a (maybe)persistent storage.
15
+ */
16
+ sealed trait StoredCheckpoint {
17
+ def id: String
18
+ def content: Checkpoint
19
+ def cleanup: Try[Unit]
20
+ }
21
+
22
+ object StoredCheckpoint {
23
+ def create(
24
+ messages: Seq[PubsubMessage],
25
+ dir: Option[String]
26
+ ): Try[StoredCheckpoint] = {
27
+ dir match {
28
+ case Some(d) =>
29
+ LocalFileStoredCheckpoint.withPersistency(d, messages)
30
+ case _ =>
31
+ val content = Checkpoint
32
+ .newBuilder()
33
+ .addAllMessages(messages.asJava)
34
+ .build()
35
+ Success(MemoryStoredStoredCheckpoint.withoutPersistency(content))
36
+ }
37
+ }
38
+
39
+ def from(id: String, persistent: Boolean): Try[StoredCheckpoint] = {
40
+ if (persistent) {
41
+ LocalFileStoredCheckpoint.from(id)
42
+ } else {
43
+ MemoryStoredStoredCheckpoint.from(id)
44
+ }
45
+ }
46
+ }
47
+
48
+ /**
49
+ * A checkpoint stored in only memory which doesn't have persistence.
50
+ *
51
+ * @param id
52
+ * @param content
53
+ */
54
+ case class MemoryStoredStoredCheckpoint private (
55
+ id: String,
56
+ content: Checkpoint
57
+ ) extends StoredCheckpoint {
58
+ import MemoryStoredStoredCheckpoint._
59
+
60
+ override def cleanup: Try[Unit] = {
61
+ storage.remove(id) match {
62
+ case Some(_) => Success(())
63
+ case _ =>
64
+ Failure(new ConfigException(s"A checkpoint ${id} is not deletable"))
65
+ }
66
+ }
67
+ }
68
+
69
+ object MemoryStoredStoredCheckpoint {
70
+ private val storage = mutable.Map[String, MemoryStoredStoredCheckpoint]()
71
+
72
+ def from(key: String): Try[MemoryStoredStoredCheckpoint] = Try(storage(key))
73
+
74
+ def withoutPersistency(content: Checkpoint): StoredCheckpoint = {
75
+ val id = content.hashCode().toString
76
+ val checkpoint = MemoryStoredStoredCheckpoint(id, content)
77
+ storage.put(id, checkpoint)
78
+ checkpoint
79
+ }
80
+
81
+ }
82
+
83
+ /**
84
+ * A checkpoint stored in local filesystem.
85
+ *
86
+ * @param id
87
+ * @param content
88
+ */
89
+ case class LocalFileStoredCheckpoint private (id: String, content: Checkpoint)
90
+ extends StoredCheckpoint {
91
+ override def cleanup: Try[Unit] = {
92
+ for {
93
+ f <- Try(new File(id))
94
+ _ <- Try(f.delete)
95
+ } yield ()
96
+ }
97
+ }
98
+
99
+ object LocalFileStoredCheckpoint {
100
+ def from(path: String): Try[LocalFileStoredCheckpoint] = {
101
+ for {
102
+ in <- Try(new FileInputStream(path))
103
+ c <- Try(Checkpoint.parseFrom(in))
104
+ } yield LocalFileStoredCheckpoint(path, c)
105
+ }
106
+
107
+ def withPersistency(
108
+ prefix: String,
109
+ messages: Seq[PubsubMessage]
110
+ ): Try[LocalFileStoredCheckpoint] = {
111
+ val path = s"${prefix}checkpoint-${messages.hashCode().toString}"
112
+ val content = Checkpoint
113
+ .newBuilder()
114
+ .addAllMessages(messages.asJava)
115
+ .build()
116
+
117
+ for {
118
+ out <- Try(new FileOutputStream(path))
119
+ _ <- Try(content.writeTo(out))
120
+ _ <- Try(out.close())
121
+ } yield LocalFileStoredCheckpoint(path, content)
122
+ }
123
+ }