Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Akka.Persistence plugin #577

Merged
merged 24 commits into from
Jan 30, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
ca7972c
initialized Akka.Persistence project and specs
Horusiath Aug 20, 2014
ba7978d
SnapshotStore, SyncJournals, GuaranteedDelivery and some GuaranteedDe…
Horusiath Aug 24, 2014
0820c35
Recovery impl
Horusiath Aug 30, 2014
6fde963
spec for guaranteed delivery failure
Horusiath Sep 17, 2014
e4cf6a6
persistence specs
Horusiath Sep 29, 2014
2e6fcd8
initial implementation of recovery State classes and behaviors
Horusiath Oct 4, 2014
c075ea5
more work on the persistence specs
Horusiath Dec 8, 2014
6fb1a77
Moved project to src/core. One new sample. More work with internal AP…
Horusiath Dec 15, 2014
c61dda4
created pseudo-state machines for Eventsourced and PersistentView
Horusiath Dec 20, 2014
38165a9
guaranteed deliver revisited, modifications for local snapshot store
Horusiath Dec 21, 2014
8f64955
Added protocol buffers to Akka.Persistence (serializers still missing…
Horusiath Dec 22, 2014
e176e4d
protobuf serializers + init commit for Akka.Persistence.TestKit
Horusiath Dec 28, 2014
199b539
Eventsourced lifecycle method overrides, ChaosJournal for random fail…
Horusiath Dec 29, 2014
0d32149
Minor bug-fixes for journals
Horusiath Dec 30, 2014
8cc72d4
virtualized ActorBase.Around* methods, created test actors for Persis…
Horusiath Jan 3, 2015
3e3f145
PersistentViewSpec and SnapshotSpec
Horusiath Jan 5, 2015
ec2301b
PersistentView snapshotting methods implemented. Technical overview d…
Horusiath Jan 8, 2015
c6e8ee8
Basic example is working!
Horusiath Jan 9, 2015
39ecfe5
Akka.Persistence.TestKit + all persistence examples fixed
Horusiath Jan 10, 2015
7fbd521
specs fixes
Horusiath Jan 17, 2015
cba1418
fixed some of the SnapshotStore and Journal TestKit tests
Horusiath Jan 22, 2015
10f6ae4
fixed all of the Akka.Persistence.TestKit specs + some specs from Akk…
Horusiath Jan 24, 2015
1f94101
Akka.Persistence .md doc, minor fixes and skip for failing tests
Horusiath Jan 29, 2015
af12e85
fixed corrupted projects to be FAKE-build compatible
Horusiath Jan 30, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions documentation/wiki/Persistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
## Persistence

Akka.Persistence plugin enables to create a statefull actors, which internal state may be stored inside persistent data storage and used for recovery in case of restart, migration or VM crash. Core concept behind Akka persistence lays in storing not only actor state directly (in form of the snapshots) but also history of all of the changes of that actor's state. This is quite useful solution common in patterns such as eventsourcing. Changes are immutable by nature, as they describe facts already reported in the history, and can be stored inside event journal in append-only mode. While recovering, actor restores it's state from the latests snapshot available - which can reduce recovery time - and then recreates it further by replaying events stored inside journal. Among other features provided by persistence plugin is support for command query segregation model and point-to-point communication with at-least-once delivery semantics.

### Architecture

Akka.Persistence features are available through new set of actor base classes:

- **PersistentActor** is a persistent, stateful equivalent of *ActorBase* class. It's able to persist event inside the journal, creating snapshots in snapshot stores and recover from them in thread-safe manner. It can be used for both changing and reading state of the actor.
- **PersistentView** is used to recreate internal state of other persistent actor based on journaled messages. It works in read-only manner - it cannot journal any event by itself.
- **GuaranteedDeliveryActor** may be used to ensure at-least-once delivery semantics between communicating actors, even in case when either sender or receiver VM crashes.
- **Journal** stores a sequence of events send by the persistent actor. The storage backend of the journal is plugable. By default it uses in-memory message stream and is NOT a persistent storage.
- **Snapshot store** is used to persist snapshots of either persistent actor's or view's internal state. They can be used to reduce recovery times in case when a lot of events needs to be replayed for specific persistent actor. Storage backend of the snapshot store is plugable. By default it uses local file system.

### Persistent actors

Unlike default `ActorBase` class `PersistentActor` and it's derivatives requires to setup a few more additional members:

- **PersistenceId** is a persistent actor's identifier that doesn't change across different actor incarnations. It's used to retrieve an event stream required by the persistent actor to recover it's internal state.
- **ReceiveRecover** is a method invoked during actor's recovery cycle. Incomming objects may be user-defined events as well as system messages, for example `SnapshotOffer` which is used to deliver latest actor state saved in the snapshot store.
- **ReceiveCommand** is an equivalent of basic `Receive` method of default Akka.NET actors.

Persistent actors also offer a set of specialized members:

- **Persist** and **PersistAsync** methods can be used to send events to event journal in order to store them inside. Second argument in a callback invoked when journal confirms, that events have been stored successfully.
- **Defer** and **DeferAsync** are used to perform various operations *after* events will be persisted and their callback handlers will be invoked. Unlike persist methods, defer won't store and event in a persistent storage. Defer methods may NOT be invoked in case when actor is restarted even thou journal will successfully persist events sent.
- **DeleteMessages** will order attached journal to remove part of it's events. It can be either logical deletion - messages are marked as deleted, but are not removed physically from the backend storage - or a physical one, when the messages are removed physically from the journal.
- **LoadSnapshot** will sent request to snapshot store to resent a current actor's snapshot.
- **SaveSnapshot** will sent current actor's internal state as snapshot to be saved by configured snapshot store.
- **DeleteSnapshot** and **DeleteSnapshots** methods may be used to specify snapshots to be removed from snapshot store in case, when they are no longer needed.
- **OnReplaySuccess** is a virtual method which will be called when recovery cycle will end successfully.
- **OnReplayFailure** is a virtual method which will be called when recovery cycle will fail unexpectedly from some reason.
- **IsRecovering** property determines if current actor is performing a recovery cycle at the moment.
- **SnapshotSequenceNr** property may be used to determine sequence number used for marking persisted events. This value is changing in monotically increasing manner.

In case when a manual recovery cycle initialization is necessary, it may be invoked by sending a `Recover` message to persistent actor.

### Persistent views

While persistent actor may be used to producing and persisting an events, views are used only to read internal state based on them. Like persistent actor, view has a **PersistenceId** to specify collection of events to be resent to current view. This value should however be correlated with PersistentId of actor - producer of the events.

Other members:

- **ViewId** property is an view unique identifier that doesn't change across different actor incarnations. It's usefull in case when there should be a multiple different views associated with a single persistent actor, but showing it's state from a different perspectives.
- **IsAutoUpdate** property determines if view will try to automatically update it's state in specified time intervals. Without it, view won't update it's state until it receives an explicit `Update` message. This value can be set through configuration with *akka.persistence.view.auto-update* set to either *on* (by default) or *off*.
- **AutoUpdateInterval** specifies time interval in which view will be updating itself - only in case when *IsAutoUpdate* flag is on. This value can be set through configuration with *akka.persistence.view.auto-update-interval* key (5 seconds by default).
- **AutoUpdateReplayMax** property determines maximum number of events to be replayed during a single *Update* cycle. This value can be set through configuration with *akka.persistence.view.auto-update-replay-max* key (by default it's -1 - no limit).
- **LoadSnapshot** will sent request to snapshot store to resent a current view's snapshot.
- **SaveSnapshot** will sent current view's internal state as snapshot to be saved by configured snapshot store.
- **DeleteSnapshot** and **DeleteSnapshots** methods may be used to specify snapshots to be removed from snapshot store in case, when they are no longer needed.

### Guaranteed delivery

Guaranteed delivery actors are specializations of persistent actors and may be used to provide at-least-once delivery semantics, even in case when one of the communication endpoints will crash. Because it's possible that the same message will be send twice, actor's receive behavior must work in the idempotent manner.

Members:

- **Deliver** method is used to send message to another actor in at-least-once delivery semantics. Message sent this way must be confirmed by the other endpoint with **ConfirmDelivery** method. Otherwise it will be resend again and again until the redelivery limit will be reached.
- **GetDeliverySnapshot** and **SetDeliverySnapshot** methods are used as part of delivery snapshotting strategy. They return/reset state of the current guaranteed delivery actor unconfirmed messages. In order to save custom deliverer state inside snapshot, a returned delivery snapshot should be included into that snapshot and reset in *ReceiveRecovery* method, when `SnapshotOffer` arrives.
- **RedeliveryBurstLimit** is a virtual property which determines maximum number of unconfirmed messages to be send in each redelivery attempt. It may be usefull to prevent message overflow scenarios. It may be overriden or configured inside HOCON configuration under *akka.persistence.at-least-once-delivery.redelivery-burst-limit* path (10 000 by default).
- **UnconfirmedDeliveryAttemptsToWarn** is a virtual property which determines, how many unconfirmed deliveries may be sent before guranteed delivery actor will send an `UnconfirmedWarning` message to itself. Count is reset after actor's restart. It may be overriden or configured inside HOCON configuration under *akka.persistence.at-least-once-delivery.warn-after-number-of-unconfirmed-attempts* path (5 by default).
- **MaxUnconfirmedMessages** is a virtual property which determines maximum of unconfirmed deliveries hold in memory. After this threshold is exceeded any **Deliver** method will raise `MaxUnconfirmedMessagesExceededException`. It may be overriden or configured inside HOCON configuration under *akka.persistence.at-least-once-delivery.max-unconfirmed-messages* path (100 000 by default).
- **UnconfirmedCount** property shows a number of the unconfirmed messages.

### Journals

Journal is a specialized type of an actor, which exposes an API to handle incoming events and store them in backend storage. By default Akka.Persitence uses a `MemoryJournal` which stores all events in memory and therefore it's not a persistent storage. Custom journal configuration path may be specified inside *akka.persistence.journal.plugin* path and by default it requires two keys set: *class* and *plugin-dispatcher*. Example configuration:

```
akka {
persistence {
journal {

# Path to the journal plugin to be used
plugin = "akka.persistence.journal.inmem"

# In-memory journal plugin.
inmem {

# Class name of the plugin.
class = "Akka.Persistence.Journal.MemoryJournal, Akka.Persistence"

# Dispatcher for the plugin actor.
plugin-dispatcher = "akka.actor.default-dispatcher"
}
}
}
}
```

### Snapshot store

Snapshot store is a specialized type of an actor, which exposes an API to handle incoming snapshot-related requests and is able to save snapshots in some backend storage. By default Akka.Persistence uses a `LocalSnapshotStore`, which uses a local file system as a storage. Custom snapshot store configuration path may be specified inside *akka.persistence.snapshot-store.plugin* path and by default it requires two keys set: *class* and *plugin-dispatcher*. Example configuration:

```
akka {
persistence {
snapshot-store {

# Path to the snapshot store plugin to be used
plugin = "akka.persistence.snapshot-store.local"

# Local filesystem snapshot store plugin.
local {

# Class name of the plugin.
class = "Akka.Persistence.Snapshot.LocalSnapshotStore, Akka.Persistence"

# Dispatcher for the plugin actor.
plugin-dispatcher = "akka.persistence.dispatchers.default-plugin-dispatcher"

# Dispatcher for streaming snapshot IO.
stream-dispatcher = "akka.persistence.dispatchers.default-stream-dispatcher"

# Storage location of snapshot files.
dir = "snapshots"
}
}
}
}
```

### Contributing

Akka persistence plugin gives a custom journal and snapshot store creator a built-in set of test, which can be used to verify correctness of the implemented backend storage plugins. It's available through `Akka.Persistence.TestKit` package and uses xUnit as default test framework.
61 changes: 60 additions & 1 deletion src/Akka.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30723.0
VisualStudioVersion = 12.0.31101.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatMessages", "examples\Chat\ChatMessages\ChatMessages.csproj", "{819221CB-82B7-43D3-A4DE-E00AF17F2FDF}"
EndProject
Expand All @@ -22,6 +22,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Protobuf", "Protobuf", "{98
core\protobuf\ContainerFormats.proto = core\protobuf\ContainerFormats.proto
core\protobuf\csharp_options.proto = core\protobuf\csharp_options.proto
core\protobuf\descriptor.proto = core\protobuf\descriptor.proto
core\protobuf\PersistenceMessages.proto = core\protobuf\PersistenceMessages.proto
core\protobuf\WireFormats.proto = core\protobuf\WireFormats.proto
EndProjectSection
EndProject
Expand Down Expand Up @@ -141,10 +142,22 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Deploy.Remote", "exa
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "FSharp.Deploy.Local", "examples\FSharp.Deploy.Local\FSharp.Deploy.Local.fsproj", "{B65AAB66-A779-4A2C-AE6B-495C292551AE}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Persistence", "Persistence", "{B17C5E75-B5F7-4492-9A97-A0C8DF66EF02}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.Tests", "core\Akka.Persistence.Tests\Akka.Persistence.Tests.csproj", "{4492004A-A8D0-45B0-BACC-05BA2F38EC55}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PersistenceExample", "examples\PersistenceExample\PersistenceExample.csproj", "{4022147A-4F95-4A04-BE09-01B7952BBDD9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "HelloWorld", "HelloWorld", "{19FD9F6D-7ED2-4DF6-98EE-348027F8D5DA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HelloAkka", "examples\HelloWorld\HelloAkka\HelloAkka.csproj", "{F09CC498-22E8-4D87-A235-CCE17E629612}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.TestKit", "core\Akka.Persistence.TestKit\Akka.Persistence.TestKit.csproj", "{AD9418B6-C452-4169-94FB-D43DE0BFA966}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence", "core\Akka.Persistence\Akka.Persistence.csproj", "{FCA84DEA-C118-424B-9EB8-34375DFEF18A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Akka.Persistence.TestKit.Tests", "core\Akka.Persistence.TestKit.Tests\Akka.Persistence.TestKit.Tests.csproj", "{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug Mono|Any CPU = Debug Mono|Any CPU
Expand Down Expand Up @@ -486,6 +499,22 @@ Global
{B65AAB66-A779-4A2C-AE6B-495C292551AE}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{B65AAB66-A779-4A2C-AE6B-495C292551AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B65AAB66-A779-4A2C-AE6B-495C292551AE}.Release|Any CPU.Build.0 = Release|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4492004A-A8D0-45B0-BACC-05BA2F38EC55}.Release|Any CPU.Build.0 = Release|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4022147A-4F95-4A04-BE09-01B7952BBDD9}.Release|Any CPU.Build.0 = Release|Any CPU
{F09CC498-22E8-4D87-A235-CCE17E629612}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{F09CC498-22E8-4D87-A235-CCE17E629612}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{F09CC498-22E8-4D87-A235-CCE17E629612}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
Expand All @@ -494,6 +523,30 @@ Global
{F09CC498-22E8-4D87-A235-CCE17E629612}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{F09CC498-22E8-4D87-A235-CCE17E629612}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F09CC498-22E8-4D87-A235-CCE17E629612}.Release|Any CPU.Build.0 = Release|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD9418B6-C452-4169-94FB-D43DE0BFA966}.Release|Any CPU.Build.0 = Release|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FCA84DEA-C118-424B-9EB8-34375DFEF18A}.Release|Any CPU.Build.0 = Release|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Debug Mono|Any CPU.ActiveCfg = Debug|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Debug Mono|Any CPU.Build.0 = Debug|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Release Mono|Any CPU.ActiveCfg = Release|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Release Mono|Any CPU.Build.0 = Release|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -554,7 +607,13 @@ Global
{68D89B2A-6770-483E-B7B0-C7EDAE0C2A6D} = {E814B432-DBC2-4372-92C6-70B9198613BD}
{F53B3F7C-6422-4AA7-AF87-B838D736DBAB} = {68D89B2A-6770-483E-B7B0-C7EDAE0C2A6D}
{B65AAB66-A779-4A2C-AE6B-495C292551AE} = {68D89B2A-6770-483E-B7B0-C7EDAE0C2A6D}
{B17C5E75-B5F7-4492-9A97-A0C8DF66EF02} = {69279534-1DBA-4115-BF8B-03F77FC8125E}
{4492004A-A8D0-45B0-BACC-05BA2F38EC55} = {01167D3C-49C4-4CDE-9787-C176D139ACDD}
{4022147A-4F95-4A04-BE09-01B7952BBDD9} = {B17C5E75-B5F7-4492-9A97-A0C8DF66EF02}
{19FD9F6D-7ED2-4DF6-98EE-348027F8D5DA} = {69279534-1DBA-4115-BF8B-03F77FC8125E}
{F09CC498-22E8-4D87-A235-CCE17E629612} = {19FD9F6D-7ED2-4DF6-98EE-348027F8D5DA}
{AD9418B6-C452-4169-94FB-D43DE0BFA966} = {01167D3C-49C4-4CDE-9787-C176D139ACDD}
{FCA84DEA-C118-424B-9EB8-34375DFEF18A} = {01167D3C-49C4-4CDE-9787-C176D139ACDD}
{E6CDDAC1-BD68-4C7B-9E6D-569AC6D50793} = {01167D3C-49C4-4CDE-9787-C176D139ACDD}
EndGlobalSection
EndGlobal
Loading