Skip to content

Commit b2fa84f

Browse files
Gustavo Fonsecagustavofonseca
authored andcommitted
Adiciona suporte transacional em Session
Pensando que um *command handler*, implementado no módulo `services.py`, podráe realizar leituras e escritas em múltiplos documentos e coleções, como ocorre em todos os que registram mudanças no feed de mudanças, este commit cria a classe `adapters.TransactionalSession` que utiliza o suporte transacional do MongoDB 4+ para múltiplas coleções. A classe `adapters.Session` também teve sua interface modificada de maneira a suportar a sintaxe de gerenciadores de contexto do Python, implementando assim a mesma interface da classe recém criada. A única restrição é que o MongoDB apenas suporta transações quando operando em replica set, e não enquanto instância *standalone*. É importante notar também que em versões anteriores a 4.4, o MongoDB necessita que as coleções sejam criadas explicitamente, e por esse motivo o comando `kernelctl create-collections` foi criado. A diretiva `kernel.app.mongodb.transactions.enabled` foi criada para tornar possĩvel usar ou não as transações. Isso é útil para ambientes de desenvolvimento onde a implantação de um *replica set* é algo indesejado. É importante ressaltar que instâncias de produção devem fazer uso de *replica sets* com o suporte transacional ligado, sob pena de ocorrer perda de dados e tornar assim a base inconsistente.
1 parent f10cdb7 commit b2fa84f

8 files changed

Lines changed: 343 additions & 195 deletions

File tree

README.md

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,18 +43,19 @@ Para mais informação sobre a nova arquitetura de sistemas de informação da M
4343
Configurando a aplicação:
4444

4545

46-
diretiva no arquivo .ini | variável de ambiente | valor padrão
47-
----------------------------------|-----------------------------------|--------------------
48-
kernel.app.mongodb.dsn | KERNEL_APP_MONGODB_DSN | mongodb://db:27017
49-
kernel.app.mongodb.dbname | KERNEL_APP_MONGODB_DBNAME | document-store
50-
kernel.app.mongodb.replicaset | KERNEL_APP_MONGODB_REPLICASET |
51-
kernel.app.mongodb.readpreference | KERNEL_APP_MONGODB_READPREFERENCE | secondaryPreferred
52-
kernel.app.mongodb.writeto | KERNEL_APP_MONGODB_WRITETO | 1
53-
kernel.app.prometheus.enabled | KERNEL_APP_PROMETHEUS_ENABLED | True
54-
kernel.app.prometheus.port | KERNEL_APP_PROMETHEUS_PORT | 8087
55-
kernel.app.sentry.enabled | KERNEL_APP_SENTRY_ENABLED | False
56-
kernel.app.sentry.dsn | KERNEL_APP_SENTRY_DSN |
57-
kernel.app.sentry.environment | KERNEL_APP_SENTRY_ENVIRONMENT |
46+
diretiva no arquivo .ini | variável de ambiente | valor padrão
47+
----------------------------------------|-----------------------------------------|--------------------
48+
kernel.app.mongodb.dsn | KERNEL_APP_MONGODB_DSN | mongodb://db:27017
49+
kernel.app.mongodb.dbname | KERNEL_APP_MONGODB_DBNAME | document-store
50+
kernel.app.mongodb.replicaset | KERNEL_APP_MONGODB_REPLICASET |
51+
kernel.app.mongodb.readpreference | KERNEL_APP_MONGODB_READPREFERENCE | secondaryPreferred
52+
kernel.app.mongodb.writeto | KERNEL_APP_MONGODB_WRITETO | 1
53+
kernel.app.mongodb.transactions.enabled | KERNEL_APP_MONGODB_TRANSACTIONS_ENABLED | False
54+
kernel.app.prometheus.enabled | KERNEL_APP_PROMETHEUS_ENABLED | True
55+
kernel.app.prometheus.port | KERNEL_APP_PROMETHEUS_PORT | 8087
56+
kernel.app.sentry.enabled | KERNEL_APP_SENTRY_ENABLED | False
57+
kernel.app.sentry.dsn | KERNEL_APP_SENTRY_DSN |
58+
kernel.app.sentry.environment | KERNEL_APP_SENTRY_ENVIRONMENT |
5859

5960

6061
A configuração padrão assume o uso de uma instância *standalone* do MongoDB. Para
@@ -66,6 +67,12 @@ deve ser definida com o nome do *replica set*. Além disso, é possível informa
6667
*seeds* do *replica set* por meio da diretiva `kernel.app.mongodb.dsn`,
6768
separando suas URIs com espaços em branco ou quebra de linha.
6869

70+
De maneira a garantir a consistência dos dados em ambiente de produção, recomenda-se que seja
71+
habilitada a diretiva `kernel.app.mongodb.transactions.enabled`. Esta diretiva depende do uso
72+
de *replica sets*. Recomenda-se o uso do MongoDB 4.4+, caso contrário tanto o banco de dados
73+
quanto as coleções terão de ser criadas explicitamente pelo DBA. Para mais detalhes acesse
74+
https://docs.mongodb.com/master/core/transactions/.
75+
6976

7077
Configurações avançadas:
7178

@@ -87,17 +94,17 @@ $ pserve development.ini
8794
Esta configuração espera uma instância de MongoDB escutando *localhost* na
8895
porta *27017*.
8996

90-
Na primeira vez será necessário criar os índices do banco de dados. Para tal
91-
execute o comando `kernelctl create-indexes`*`mongo-db-dsn dbname`*.
97+
Na primeira vez será necessário criar a estrutura e os índices do banco de dados. Para tal
98+
execute o comando `kernelctl create-collections`*`mongo-db-dsn dbname`*` | kernelctl create-indexes`*`mongo-db-dsn dbname`*.
9299

93100

94101
### Executando via Docker:
95102

96103
`$ docker-compose up -d`
97104

98-
Na primeira vez será necessário criar os índices do banco de dados:
105+
Na primeira vez será necessário criar a estrutura e os índices do banco de dados:
99106

100-
`$ docker-compose exec webapp kernelctl create-indexes`*`mongo-db-dsn dbname`*
107+
`$ docker-compose exec webapp kernelctl create-collections`*`mongo-db-dsn dbname`*` | kernelctl create-indexes`*`mongo-db-dsn dbname`*
101108

102109

103110
Testando o registro de um documento de exemplo:

development.ini

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ pyramid.debug_routematch = false
88
pyramid.debug_templates = true
99
pyramid.default_locale_name = en
1010

11-
kernel.app.mongodb.dsn=mongodb://localhost:27017
11+
;kernel.app.mongodb.dsn=mongodb://localhost:27017
1212
;kernel.app.mongodb.replicaset=
1313
;kernel.app.mongodb.readpreference=
14+
;kernel.app.mongodb.writeto=
15+
;kernel.app.mongodb.transactions.enabled=
1416
;kernel.app.prometheus.enabled=
1517
;kernel.app.prometheus.port=
1618
;kernel.app.sentry.enabled=

documentstore/adapters.py

Lines changed: 106 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@ class MongoDB:
3434
https://api.mongodb.com/python/current/api/pymongo/mongo_client.html
3535
"""
3636

37-
def __init__(
38-
self, uri, dbname, mongoclient=pymongo.MongoClient, options=None,
39-
):
37+
def __init__(self, uri, dbname, mongoclient=pymongo.MongoClient, options=None):
4038
self._dbname = dbname
4139
self._uri = uri
4240
self._MongoClient = mongoclient
@@ -92,30 +90,104 @@ def create_indexes(self):
9290
[("timestamp", pymongo.ASCENDING)], unique=True, background=True
9391
)
9492

93+
def start_session(self):
94+
"""Inicia uma sessão transacional.
95+
"""
96+
return self._client.start_session()
97+
98+
def start_transaction(self):
99+
"""Inicia uma transação.
100+
"""
101+
return self._client.start_transaction()
102+
103+
def create_collections(self):
104+
"""Cria as coleções na base de dados.
105+
106+
Com o uso de transações em instâncias de MongoDB < 4.4 as coleções não
107+
podem ser criadas implicitamente.
108+
"""
109+
for colname in ["documents", "documents_bundles", "journals", "changes"]:
110+
self._db().create_collection(colname)
111+
95112

96113
class Session(interfaces.Session):
97114
"""Implementação de `interfaces.Session` para armazenamento em MongoDB.
98-
Trata-se de uma classe concreta e não deve ser generalizada.
115+
116+
Instâncias de :class:`Session` servem como pontos de acesso aos repositórios
117+
de dados. Instâncias desta classe poderão usar da sintaxe de gerenciadores
118+
de contexto do Python, mas sem qualquer efeito. Caso
99119
"""
100120

101121
def __init__(self, mongodb_client):
102122
self._mongodb_client = mongodb_client
103123

124+
def __enter__(self):
125+
return self
126+
127+
def __exit__(self, exc_type, exc_value, traceback):
128+
pass
129+
130+
def _repo_extra_args(self):
131+
"""Argumentos extras que serão passados na inicialização dos
132+
repositórios.
133+
"""
134+
return {}
135+
104136
@property
105137
def documents(self):
106-
return DocumentStore(self._mongodb_client.documents)
138+
return DocumentStore(self._mongodb_client.documents, **self._repo_extra_args())
107139

108140
@property
109141
def documents_bundles(self):
110-
return DocumentsBundleStore(self._mongodb_client.documents_bundles)
142+
return DocumentsBundleStore(
143+
self._mongodb_client.documents_bundles, **self._repo_extra_args()
144+
)
111145

112146
@property
113147
def journals(self):
114-
return JournalStore(self._mongodb_client.journals)
148+
return JournalStore(self._mongodb_client.journals, **self._repo_extra_args())
115149

116150
@property
117151
def changes(self):
118-
return ChangesStore(self._mongodb_client.changes)
152+
return ChangesStore(self._mongodb_client.changes, **self._repo_extra_args())
153+
154+
155+
class TransactionalSession(Session):
156+
"""Implementação de `interfaces.Session` para armazenamento em MongoDB, com
157+
suporte transacional de múltiplas coleções.
158+
159+
Instâncias de :class:`TransactionalSession` servem como pontos de acesso aos
160+
repositórios de dados. Elas podem ser utilizadas na realização de consultas
161+
avulsas aos dados ou mais sofisticadas, em contextos transacionais. Caso o
162+
último seja desejado, deve-se instanciar :class:`TransactionalSession` com a
163+
sintaxe de gerenciadores de contexto do Python.
164+
"""
165+
166+
def __init__(self, mongodb_client):
167+
self._mongodb_client = mongodb_client
168+
self._txn_session = None
169+
170+
def __enter__(self):
171+
self._txn_session = self._mongodb_client.start_session()
172+
self._txn_session.start_transaction()
173+
LOGGER.debug("new MongoDB transactional session created: %s", self._txn_session)
174+
return self
175+
176+
def __exit__(self, exc_type, exc_value, traceback):
177+
if exc_type:
178+
self._txn_session.abort_transaction()
179+
LOGGER.debug(
180+
'transaction "%s" was aborted: %s', self._txn_session, exc_value
181+
)
182+
else:
183+
self._txn_session.commit_transaction()
184+
LOGGER.debug('transaction "%s" was commited', self._txn_session)
185+
186+
def _repo_extra_args(self):
187+
"""Argumentos extras que serão passados na inicialização dos
188+
repositórios.
189+
"""
190+
return {"txn_session": self._txn_session}
119191

120192

121193
class BaseStore(interfaces.DataStore):
@@ -124,8 +196,15 @@ class BaseStore(interfaces.DataStore):
124196
implementam/definem o atributo `DomainClass`.
125197
"""
126198

127-
def __init__(self, collection):
199+
def __init__(self, collection, txn_session=None):
128200
self._collection = collection
201+
self._txn_session = txn_session
202+
203+
def _txn_session_arg(self):
204+
if self._txn_session:
205+
return {"session": self._txn_session}
206+
else:
207+
return {}
129208

130209
def _pre_write(self, data) -> dict:
131210
"""Tratamento anterior ao armazenamento do dado no MongoDB."""
@@ -141,22 +220,24 @@ def _post_read(self, data: dict) -> dict:
141220
def add(self, data) -> None:
142221
try:
143222
_, _manifest = self._pre_write(data)
144-
self._collection.insert_one(_manifest)
223+
self._collection.insert_one(_manifest, **self._txn_session_arg())
145224
except pymongo.errors.DuplicateKeyError:
146225
raise exceptions.AlreadyExists(
147226
"cannot add data with id " '"%s": the id is already in use' % data.id()
148227
) from None
149228

150229
def update(self, data) -> None:
151230
_id, _manifest = self._pre_write(data)
152-
result = self._collection.replace_one({"_id": _id}, _manifest)
231+
result = self._collection.replace_one(
232+
{"_id": _id}, _manifest, **self._txn_session_arg()
233+
)
153234
if result.matched_count == 0:
154235
raise exceptions.DoesNotExist(
155236
"cannot update data with id " '"%s": data does not exist' % data.id()
156237
)
157238

158239
def fetch(self, id: str):
159-
manifest = self._collection.find_one({"_id": id})
240+
manifest = self._collection.find_one({"_id": id}, **self._txn_session_arg())
160241
if manifest:
161242
return self.DomainClass(manifest=self._post_read(manifest))
162243
else:
@@ -170,12 +251,19 @@ class ChangesStore(interfaces.ChangesDataStore):
170251
MongoDB.
171252
"""
172253

173-
def __init__(self, collection):
254+
def __init__(self, collection, txn_session=None):
174255
self._collection = collection
256+
self._txn_session = txn_session
257+
258+
def _txn_session_arg(self):
259+
if self._txn_session:
260+
return {"session": self._txn_session}
261+
else:
262+
return {}
175263

176264
def add(self, change: dict):
177265
try:
178-
self._collection.insert_one(change)
266+
self._collection.insert_one(change, **self._txn_session_arg())
179267
except pymongo.errors.DuplicateKeyError as exc:
180268
raise exceptions.AlreadyExists(
181269
'cannot add data with id "%s": %s' % (change["_id"], exc)
@@ -186,11 +274,14 @@ def filter(self, since: str = "", limit: int = 500):
186274
{"timestamp": {"$gt": since}},
187275
sort=[("timestamp", pymongo.ASCENDING)],
188276
projection={"content_gz": False, "content_type": False},
277+
**self._txn_session_arg(),
189278
).limit(limit)
190279

191280
def fetch(self, id: str) -> dict:
192281
try:
193-
change = self._collection.find_one({"_id": ObjectId(id)})
282+
change = self._collection.find_one(
283+
{"_id": ObjectId(id)}, **self._txn_session_arg()
284+
)
194285
except bson.errors.InvalidId as exc:
195286
raise exceptions.DoesNotExist(
196287
'cannot fetch data with id "%s": %s' % (id, exc)

documentstore/interfaces.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,16 @@ class Session(abc.ABC):
5151
def partial(cls, *args, **kwargs):
5252
return functools.partial(cls, *args, **kwargs)
5353

54+
def __enter__(self):
55+
"""Inicialização do contexto transacional.
56+
"""
57+
return self
58+
59+
def __exit__(self, exc_type, exc_value, traceback):
60+
"""Finalização do contexto transacional.
61+
"""
62+
pass
63+
5464
@property
5565
@abc.abstractmethod
5666
def documents(self) -> DataStore:

documentstore/kernelctl.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def _create_indexes(args):
2424
mongo.create_indexes()
2525

2626

27+
def _create_collections(args):
28+
mongo = adapters.MongoDB(args.dsn, args.dbname)
29+
mongo.create_collections()
30+
31+
2732
def cli(argv=None):
2833
if argv is None:
2934
argv = sys.argv
@@ -47,6 +52,18 @@ def cli(argv=None):
4752
parser_create_indexes.add_argument("dbname", help="Database name.")
4853
parser_create_indexes.set_defaults(func=_create_indexes)
4954

55+
parser_create_collections = subparsers.add_parser(
56+
"create-collections",
57+
help="Create all database collections",
58+
description="Explicitly creates all database collections. "
59+
"This is required when using MongoDB < 4.4 with the transactional support enabled.",
60+
)
61+
parser_create_collections.add_argument(
62+
"dsn", help="DSN for MongoDB node where collections will be created."
63+
)
64+
parser_create_collections.add_argument("dbname", help="Database name.")
65+
parser_create_collections.set_defaults(func=_create_collections)
66+
5067
args = parser.parse_args()
5168
# todas as mensagens serão omitidas se level > 50
5269
logging.basicConfig(

documentstore/restfulapi.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1188,6 +1188,12 @@ def split_dsn(dsns):
11881188
),
11891189
("kernel.app.mongodb.writeto", "KERNEL_APP_MONGODB_WRITETO", int, 1),
11901190
("kernel.app.mongodb.dbname", "KERNEL_APP_MONGODB_DBNAME", str, "document-store"),
1191+
(
1192+
"kernel.app.mongodb.transactions.enabled",
1193+
"KERNEL_APP_MONGODB_TRANSACTIONS_ENABLED",
1194+
asbool,
1195+
False,
1196+
),
11911197
("kernel.app.prometheus.enabled", "KERNEL_APP_PROMETHEUS_ENABLED", asbool, True),
11921198
("kernel.app.prometheus.port", "KERNEL_APP_PROMETHEUS_PORT", int, 8087),
11931199
("kernel.app.sentry.enabled", "KERNEL_APP_SENTRY_ENABLED", asbool, False),
@@ -1239,7 +1245,15 @@ def main(global_config, **settings):
12391245
"w": settings["kernel.app.mongodb.writeto"],
12401246
},
12411247
)
1242-
Session = adapters.Session.partial(mongo)
1248+
1249+
if settings["kernel.app.mongodb.transactions.enabled"]:
1250+
Session = adapters.TransactionalSession.partial(mongo)
1251+
else:
1252+
Session = adapters.Session.partial(mongo)
1253+
LOGGER.warning(
1254+
"transactional support is disabled and it may cause data loss. "
1255+
"If the app is running in production, this is a huge mistake"
1256+
)
12431257

12441258
config.add_request_method(
12451259
lambda request: services.get_handlers(Session), "services", reify=True

0 commit comments

Comments
 (0)