Skip to content

Commit

Permalink
Emulate
Browse files Browse the repository at this point in the history
Emulate
  • Loading branch information
lazamar authored Nov 6, 2024
2 parents 9e41b2a + 60564ad commit 6ee56b0
Show file tree
Hide file tree
Showing 25 changed files with 1,240 additions and 305 deletions.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,86 @@
A local version of Ambar for testing and initial integration.

Connect your databases to multiple consumers with minimal configuration and no libraries needed.

## Configuration

A YAML configuration file should describe all sources and destinations to be used.

``` yaml
# Connections to your databases.
# The Emulator will read data from those dbs.
data_sources:
- id: postgres_source
description: Main events store
type: postgres
host: localhost
port: 5432
username: my_user
password: my_pass
database: my_db
table: events_table
columns:
- id
- aggregate_id
- sequence_number
- payload
serialColumn: id
partitioningColumn: aggregate_id

# Connections to your endpoint.
# The Emulator will send data read from the databases to these endpoints.
data_destinations:

# Send data via HTTP
- id: http_destination
description: my projection 2
type: http-push
endpoint: http://some.url.com:8080/my_projection
username: name-of-user
password: password123

sources:
- postgres source
- file source

# Send data to a file. One entry per line.
- id: file_destination
description: my projection 1
type: file
path: ./temp.file

sources:
- postgres_source
- file_source
```
## Running the program
You only need to provide the address of the configuration file and the emulator
will start streaming your data.
``` bash
> emulator run --config config.yaml
```

## Help

You can see all commands available with the `--help` flag

``` bash
Ambar Emulator v0.0.1 - alpha release

A local version of Ambar <https://ambar.cloud>
Connect your databases to multiple consumers with minimal configuration and no libraries needed.

Usage: emulator COMMAND

Available options:
--version Show version information
-h,--help Show this help text

Available commands:
run run the emulator

More info at <https://github.com/ambarltd/emulator>
```
13 changes: 13 additions & 0 deletions emulator.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ executable emulator
other-modules:
build-depends:
base ^>=4.17.2.1
, directory
, emulator-lib
, filepath
, prettyprinter
, prettyprinter-ansi-terminal
, optparse-applicative
Expand All @@ -34,6 +36,9 @@ library emulator-lib
ghc-options: -Wall -Werror -fprof-auto
hs-source-dirs: src
exposed-modules:
Ambar.Emulator
Ambar.Emulator.Config
Ambar.Emulator.Connector.File
Ambar.Emulator.Connector.Poll
Ambar.Emulator.Connector.Postgres
Ambar.Emulator.Projector
Expand All @@ -44,9 +49,11 @@ library emulator-lib
Ambar.Emulator.Queue.Partition.STMReader
Ambar.Transport
Ambar.Transport.Http
Ambar.Transport.File
Utils.Async
Utils.Delay
Utils.Logger
Utils.Prettyprinter
Utils.STM
Utils.Some
build-depends:
Expand All @@ -59,6 +66,7 @@ library emulator-lib
, binary
, casing
, containers
, data-default
, directory
, extra
, filepath
Expand All @@ -73,15 +81,18 @@ library emulator-lib
, text
, time
, unordered-containers
, yaml

test-suite emulator-tests
import: common
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wall -Werror
hs-source-dirs: tests
main-is: Tests.hs
other-modules:
Test.Config
Test.Queue
Test.Connector
Test.Emulator
Test.Utils.Tests
Test.Utils.OnDemand
type: exitcode-stdio-1.0
Expand All @@ -92,6 +103,7 @@ test-suite emulator-tests
, async
, bytestring
, containers
, data-default
, directory
, filepath
, hspec
Expand All @@ -101,6 +113,7 @@ test-suite emulator-tests
, process
, QuickCheck
, stm
, string-interpolate
, temporary
, text
, unordered-containers
Expand Down
61 changes: 37 additions & 24 deletions examples/config.yml
Original file line number Diff line number Diff line change
@@ -1,32 +1,45 @@
# Connections to your databases.
# The Emulator will read data from those dbs.
data_sources:
- id: "postgres source"
type: "postgres"
host: "localhost"
username: "temp"
password: "some_pass"
database: "db_name"
table: "table_name"

- id: "file source"
type: "file"
path: "./source.txt"
- id: postgres_source
description: Main events store
type: postgres
host: localhost
port: 5432
username: my_user
password: my_pass
database: my_db
table: events_table
columns:
- id
- aggregate_id
- sequence_number
- payload
serialColumn: id
partitioningColumn: aggregate_id

# Connections to your endpoint.
# The Emulator will send data read from the databases to these endpoints.
data_destinations:
- id: "file destination"
type: "file"
path: "./temp.file"

# Send data via HTTP
- id: http_destination
description: my projection 2
type: http-push
endpoint: http://some.url.com:8080/my_projection
username: name-of-user
password: password123

sources:
- id: "postgres source"
- id: "file source"
- postgres source
- file source

- id: "HTTP destination"
type: "http-push"
endpoint: http://some.url.com/one
password: password123
port: 22
username: name-of-user
# Send data to a file. One entry per line.
- id: file_destination
description: my projection 1
type: file
path: ./temp.file

sources:
- id: "postgres source"
- id: "file source"
- postgres_source
- file_source
144 changes: 144 additions & 0 deletions src/Ambar/Emulator.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
module Ambar.Emulator where

import Control.Concurrent.STM (atomically)
import Control.Concurrent.Async (concurrently_, forConcurrently_, withAsync)
import Control.Exception (finally, uninterruptibleMask_, throwIO, ErrorCall(..))
import Control.Monad (forM)
import Data.Aeson (FromJSON, ToJSON)
import qualified Data.Aeson as Aeson
import Data.Default (def)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (fromMaybe)
import Foreign.Marshal.Utils (withMany)
import GHC.Generics (Generic)
import System.Directory (doesFileExist)
import System.FilePath ((</>))

import qualified Ambar.Emulator.Connector.Postgres as Postgres
import qualified Ambar.Emulator.Connector.File as FileConnector

import qualified Ambar.Emulator.Projector as Projector
import Ambar.Emulator.Projector (Projection(..))
import qualified Ambar.Transport.File as FileTransport
import qualified Ambar.Transport.Http as HttpTransport
import qualified Ambar.Emulator.Queue.Topic as Topic
import Ambar.Emulator.Queue (TopicName(..))
import qualified Ambar.Emulator.Queue as Queue
import Ambar.Emulator.Config
( EmulatorConfig(..)
, EnvironmentConfig(..)
, Id(..)
, DataSource(..)
, Source(..)
, DataDestination(..)
, Destination(..)
)
import Utils.Logger (SimpleLogger, annotate)
import Utils.Some (Some(..))
import Utils.Delay (every, seconds)

data ConnectorState
= StatePostgres Postgres.ConnectorState
| StateFile ()
deriving (Generic)
deriving anyclass (ToJSON, FromJSON)

newtype EmulatorState = EmulatorState
{ connectors :: Map (Id DataSource) ConnectorState
}
deriving (Generic)
deriving anyclass (ToJSON, FromJSON)

emulate :: SimpleLogger -> EmulatorConfig -> EnvironmentConfig -> IO ()
emulate logger config env = do
Queue.withQueue queuePath pcount $ \queue ->
concurrently_ (connectAll queue) (projectAll queue)
where
queuePath = c_dataPath config </> "queues"
statePath = c_dataPath config </> "state.json"
pcount = Topic.PartitionCount $ c_partitionsPerTopic config

connectAll queue = do
EmulatorState connectorStates <- load
let getState source =
fromMaybe (initialStateFor source) $
Map.lookup (s_id source) connectorStates

sources =
[ (source, getState source) | source <- Map.elems $ c_sources env ]

withMany (connect queue) sources $ \svars ->
every (seconds 30) (save svars) `finally` save svars

load = do
exists <- doesFileExist statePath
if not exists
then return (EmulatorState def)
else do
r <- Aeson.eitherDecodeFileStrict statePath
case r of
Right v -> return v
Left err ->
throwIO $ ErrorCall $ "Unable to decode emulator state: " <> show err

save svars =
uninterruptibleMask_ $ do
-- reading is non-blocking so should be fine to run under uninterruptibleMask
states <- forM svars $ \(sid, svar) -> (sid,) <$> atomically svar
Aeson.encodeFile statePath $ EmulatorState (Map.fromList states)

connect queue (source, sstate) f = do
topic <- Queue.openTopic queue $ topicName $ s_id source
case s_source source of
SourcePostgreSQL pconfig -> do
let logger' = annotate ("source: " <> unId (s_id source)) logger
partitioner = Postgres.partitioner
encoder = Postgres.encoder pconfig
state <- case sstate of
StatePostgres s -> return s
_ -> throwIO $ ErrorCall $
"Incompatible state for source: " <> show (s_id source)
Topic.withProducer topic partitioner encoder $ \producer ->
Postgres.withConnector logger' state producer pconfig $ \stateVar ->
f (s_id source, StatePostgres <$> stateVar)

SourceFile path ->
Topic.withProducer topic FileConnector.partitioner FileConnector.encoder $ \producer ->
withAsync (FileConnector.connect logger producer path) $ \_ -> do
f (s_id source, return $ StateFile ())

initialStateFor source =
case s_source source of
SourcePostgreSQL _ -> StatePostgres def
SourceFile _ -> StateFile ()

projectAll queue = forConcurrently_ (c_destinations env) (project queue)

project queue dest =
withDestination dest $ \transport -> do
sourceTopics <- forM (d_sources dest) $ \sid -> do
topic <- Queue.openTopic queue (topicName sid)
return (sid, topic)
Projector.project logger Projection
{ p_id = projectionId (d_id dest)
, p_destination = d_id dest
, p_sources = sourceTopics
, p_transport = transport
}

withDestination dest act =
case d_destination dest of
DestinationFile path ->
FileTransport.withFileTransport path (act . Some)
DestinationHttp{..} -> do
transport <- HttpTransport.new d_endpoint d_username d_password
act (Some transport)
DestinationFun f -> do
act (Some f)

topicName :: Id DataSource -> TopicName
topicName sid = TopicName $ "t-" <> unId sid

projectionId :: Id DataDestination -> Id Projection
projectionId (Id dst) = Id ("p-" <> dst)
Loading

0 comments on commit 6ee56b0

Please sign in to comment.