Clone the project and enter on its folder:
$ git clone [email protected]:fecaps/email_microservice.git && \
cd email_microservice
Copy env variables file and edit it in case of willing to change its configuration:
$ cp .env.example .env
** These env variables require API keys, versions and secrets related to the email vendors used within the application:
- Build Docker image in detach mode and run it:
$ docker-compose -f infrastructure/docker-compose.yml up --build -d
The default API host:port is:
The console command responsible for sending emails to the queue:
docker exec -it infrastructure_email_1 php artisan create:email
The project has these composer
scripts:
PS.: It requires the containers running
composer run-script codeStyle
# code style check
composer run-script copyPasteDetector
# mess detector
composer run-script messDetector
# copy/paste detector
composer run-script objectCalisthenics
# object calisthenics rules
composer run-script errorsAnalyse
# errors analyse
composer run-script fixStyle
# fix style
PS.: It requires the container running
- Running tests:
$ composer run-script tests
The tests generate a HTML and TXT reports which use XDebug and it's
located on report
folder.
- Showing code coverage in TXT:
$ composer run-script showCoverage
In case of willing to see it in HTML, open report/index.html
file in host machine. Example:
$ google-chrome report/index.html
There are two git hooks, which are composed of composer scripts and testing scripts.
-
pre-commit
:codeStyle
copyPasteDetector
messDetector
objectCalisthenics
-
pre-push
:codeStyle
copyPasteDetector
messDetector
objectCalisthenics
tests
showCoverage
The project is composed of 4 resources:
infrastructure_email
(the publisher - stateless)infrastructure_email_consumer
(the queue consumer - stateless)email_rabbitmq
(the queue consumer - stateful)email_nginx
(the queue consumer - stateless)infrastructure_postgres_1
(the queue messages status - stateful)
An implementation of MailerTransactor
. At the moment the application uses two
transactors/vendors for delivering emails (in this order):
- Mailjet
- Sendgrid
Attention
These two vendors responsible for delivering emails have daily and monthly limits.
Mailjet has a limit of 200 requests/day.
Sendgrid has a limit of 100 requests/day.
Plus these limits, Mailjet requires to add sender addresses in their platform,
so it's safer to add both [email protected]
and [email protected]
on it (which are used by the tests).
The email worker has an array of MailerTransactors
and it tries to send an email
through each one, in case of one failing then it goes to the next,
in case of success it finishes the worker with a boolean true
, making it clear
the email was sent. If none vendor worked, then the worker finishes with a false
.
In case of being needed to add a new vendor then these are the steps required:
- Create a config for the new vendor service in
config/service.php
file. Like the API key, etc. - Create the env variables used by the new vendor service.
- Create a connector class for the new vendor service
- Example: the
Mailjet
connector creates an instance of aMailjet Client
(third-party vendor).
- Example: the
- Create a singleton instance for the new connector created (in
EmailServiceProvider
). - Create a transactor class for the new vendor service. This transactor will be responsible for
sending the email only. This transactor must implements
MailerTransactor
interface. - Add the new transactor to the
AppServiceProvider
, as a dependency in theEmailWorker
bind.
Laravel officially supports relational databases and Redis as stateful resources for dealing with queueing, however this project uses RabbitMQ, a message broker for queuing, these are the reasons:
-
When compared to any relational databases it's easier to add nodes to the RabbitMQ cluster and natively deal with which message been managed by only one consumer. In a relational database a lock field/layer on the request should be added to deal with it. Therefore it's easier to scale up with RabbitMQ.
-
When having network issues/breaks before actually acknowledging a message in a consumer the broker itself manages to requeue the message. In a relational database this would be managed by some tool/framework/implementation.
-
When compared to any relational databases it's easier to deal with race conditions, as some configurations like
prefetch
can be set in order to facilitate this. -
When compared to Redis it's safer, as all data (exchanges, queues and messages) can be set as
durable/persistent
(to save in the disk), this way a broker restart wouldn't cause all messages to be lost. While an in-memory queueing doesn't prevent this, plus in a cluster (for instance, through Kubernetes) more memory would be required when scaling up, while by using disks some volumes can be added.
Despite the usage of RabbitMQ for queueing the messages status are kept in Postgres container, this in order to allow actions like retrieving which messages have been delivered, etc.
- Package used to deal with AMQP in Laravel: https://github.com/bschmitt/laravel-amqp
Possible improvements:
- Better usage of AMQP connections and channel by choosing whether both should be closed when publishing or consuming a message.
- Improve prefetch configuration for channels.
- Add support for custom properties (headers) when publishing messages, this way can be possible to set messages TTL, retries, etc.
As this microservice is still small there are no third-party services, such as Graylog (Mongo, Elasticsearch) to deal with logging. They are still managed through log files (which are docker volumes).
Publisher logs:
/storage/logs/publisher.log
Consumer logs:
/storage/logs/consumer.log
- Docker: All files related to Docker and docker-compose
are set within
infrastructure
folder.
The infrastructure_email_1
and infrastructure_email_consumer_1
resources have multi-stage builds.
Which are composed of two steps:
- Composer
- Installing PHP extensions, Composer dependencies and configuring web/app server
There are two Dockerfiles
, one is used for
development
and another for production
.
The one used for development
contains XDebug
and dev dependencies.
All stateless can be scaled horizontally. Examples:
- Scaling up email consumers to 3:
$ docker-compose -f infrastructure/docker-compose.yml up --scale email_consumer=3 --build
- Scaling up email app servers to 2 and consumers to 3:
** Each app server added requires adding server infrastructure_email_{count}:9000;
to
the upstream
config on Nginx file (infrastructure/nginx/default.conf
).
$ docker-compose -f infrastructure/docker-compose.yml up --scale email=2 --scale email_consumer=3 --build
- Create a frontend application to list the messages based on endpoint created above
- Create a form in the frontend in order to also create new emails
- Add Swagger for API documentation
- Add a repository for the
Queue
entity - Add Kubernetes to the stateful and stateless resources
Host: http://localhost:8080
-
Get all email transactions
- Resource:
GET /emails
- Resource:
-
Create email resource
-
Resource:
POST /emails
-
Input Payload:
- Example of text content:
{ "from": { "email": "[email protected]", "name": "test" }, "to": [ { "email": "[email protected]", "name": "test1" } ], "subject": "hello - test", "textPart": "hello - text test" }
- Example of html content:
{ "from": { "email": "[email protected]", "name": "test" }, "to": [ { "email": "[email protected]", "name": "test1" } ], "subject": "hello - test", "htmlPart": "hello<br><br>html test" }
- Example of markdown content:
{ "from": { "email": "[email protected]", "name": "test" }, "to": [ { "email": "[email protected]", "name": "test1" } ], "subject": "hello - test", "markdownPart": "hello, **markdown** test" }
-