-
Notifications
You must be signed in to change notification settings - Fork 7
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
pos-print: add async receipt printing #65
base: master
Are you sure you want to change the base?
Conversation
@@ -39,6 +40,10 @@ public static void main(String[] args) { | |||
new PersistentModule(client, commandCLI.dbName()) | |||
); | |||
|
|||
BackgroundReceiptPrinter backgroundPrinter = injector.getInstance(BackgroundReceiptPrinter.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use guava services about this as it will enforce lifecycle (start/stop) of the printing service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented Guava services for the background thread.
*/ | ||
interface BackgroundReceiptPrinter { | ||
fun run() | ||
fun printReceipts(collection: MongoCollection<Document>) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BackgroundReceiptPrinter interface gets coupled to mongodb which doesn't sound good. It would be better db to be passed to the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will implement an interface to abstract the receipt queue from the background thread and that should clear up these dependencies.
|
||
override fun run() { | ||
logger.info("Starting background printing service") | ||
repository.getCappedCollection().drop() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This drop will cause issues If app is re-started and some receipts are not printed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All of the receipts are stored in another collection before being passed to the capped one, so none would be lost, the drop is there because a restart would print all of the contained receipts and removing a single receipt from the capped collection isn't allowed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of a capped collection here seemed to mimic a normal Java queue as it would need to be dropped at the start and end of the program. On top of that, the mongo tailable cursor was not behaving properly so I switched the implementation for a plain in memory queue that block when asked for the next element and the functionality has remained the same with the bonus of loosing Mongo's capped collection restrictions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using of in-memory queue is not a good idea as it doesn't allow multiple instances of the pos-print service to be used which removes the redundancy.
Take a look into this one:
http://learnmongodbthehardway.com/schema/queues/
A queue in the setting of MongoDB, means a collection where inserted documents are kept in an order, either by the creation data of the document or by a priority assigned to the document. Multiple publishers can be inserting new documents into the queue at the same time, but each document can only be delivered to a single listener.
e.g this is what is expected from search service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Multiple instances would all have their own in-memory blocking queue, why would there be a need for a shared queue between the instances?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Queue separation per IP my do it actually.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also with this in memory queue we can implement logic on the app start, that checks for a not printed receipts and then schedule them in the queue, like that if we restart the app it will load the not printed receipts and trey to print them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the CappedCollections required the both operations : put in the collection and put in the capped collection to be executed in a transaction(I'm not a hundred 100% sure)
Yes, it's not atomic now. I suppose it would be better single not capped collection to be used instead as till findAndModify may update it in atomic manner.
Other solution could be creating separate queue for each pos-printer.
Instances have to take free printer and use it ... this adds a lot of complexity and this is why it would be better to be in-memory.
As final: the in-memory solution sounds reasonable for know. If we encounter issues then we will may consider implementing another options.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree with you. At this time the app works syncronous and there a just a few problems.
I think that we should provide and a rest services, to serve the last recepts and a rest service to schedule already saved receipt for printing again(failed receipts). Like that we could provide simple table with that contains last 50 receipts and show button for rescheduling of the failed.
oneOf(notifier).notifyPrinted(acceptedPrintingResponse) | ||
} | ||
|
||
collection.insertOne(receiptDoc) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks really strange to be tested like that as the test knows the structure of the receipt in the queue. The test shouldn't know this and should deal with Receipt object or ReceiptScheduledForPrinting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Separated mongo logic from the background thread's dependencies.
fun requeueingQueuedReceiptThrowsException() { | ||
repository.queue(anyReceipt, sourceIp, isFiscal) | ||
|
||
Thread.sleep(500) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sleep is not a good idea in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed thread sleeps from tests.
} | ||
|
||
@Suppress("UNCHECKED_CAST") | ||
private fun adaptReceipt(receiptDoc: Document): Receipt { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should work with the documents only in the persistence adapter.
You can have PrinterQueue interfece
interface PrinterQueue {
fun first():Receipt
fun add(receipt: Receipt)
fun clear()
}
and you should work with it here in the logic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented a PrintQueue interface that provides methods to add a receipt, check for a next receipt, get the next receipt and clear itself.
f4b2d07
to
8266969
Compare
PR is up to date with requested changes. |
8266969
to
eb9fac6
Compare
var cleaner = DatastoreCleaner(dataStoreRule.db()) | ||
|
||
|
||
private fun Mockery.expecting(block: Expectations.() -> Unit) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Private function should be last in the file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved private functions to the bottom of the file.
* | ||
* @author Tsvetozar Bonev ([email protected]) | ||
*/ | ||
interface PrintNotifier { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PrintNotifier doesn't sound good as interface. PrintingListener sounds better as name.
onReceiptPrinted(receipt: Receipt, printStatus: PrintStatus)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed interface name to PrintingListener and implemented the suggested method signature.
* Checks if there is a next receipt, blocks until a next | ||
* one is found. | ||
*/ | ||
fun hasNext(): Boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure that you need hasNext. next() could block.
For example take a look into: https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/ArrayBlockingQueue.html#poll--
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Implemented blocking via the ArrayBlockingQueue take() method.
HttpBackend backend = new HttpBackend(commandCLI.httpPort(), injector); | ||
backend.start(); | ||
|
||
ReceiptPrintWorker backgroundPrinter = injector.getInstance(ReceiptPrintWorker.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReceiptPrintingService sounds better then worker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed to ReceiptPrintingService.
HttpBackend backend = new HttpBackend(commandCLI.httpPort(), injector); | ||
backend.start(); | ||
|
||
ReceiptPrintWorker backgroundPrinter = injector.getInstance(ReceiptPrintWorker.class); | ||
backgroundPrinter.start(); | ||
|
||
System.out.printf("POS Print Service is up and running on port: %d\n", commandCLI.httpPort()); | ||
|
||
Runtime.getRuntime().addShutdownHook(new Thread(() -> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have to shutdown printing service when termination signal is received.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
awaitTerminated() won't return, is it necessary for it to be called or should I just leave it as stopAsync()?
Reply.with(receiptStatus).ok() | ||
}catch (ex: ReceiptNotInQueueException){ | ||
logger.error("Receipt not found") | ||
Reply.saying<Int>().notFound() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
saying<Int>
is strange. You can use Any or Unit I suppose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to be saying.
override fun printReceipts() { | ||
while (queue.hasNext()) { | ||
val receiptWithSource = queue.next() | ||
println(receiptWithSource) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logger could be used instead of println
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed forgotten testing printlns from code.
if (printResponse.warnings.contains(Status.FISCAL_RECEIPT_IS_OPEN) | ||
|| printResponse.warnings.contains(Status.NON_FISCAL_RECEIPT_IS_OPEN)) { | ||
logger.info("Receipt printing accepted") | ||
repository.finishPrinting(receipt.receiptId) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The repository could be responsible for the notification. This is going to remove notifier/listener dependency from here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think the repository should be responsible for the notification, as the listener handles a PrintReceiptResponse which the repository otherwise doesn't have to know about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are partially right but:
Here you have 4 dependencies:
class BackgroundReceiptPrintWorker @Inject constructor(private var repository: ReceiptRepository,
private var factory: PrinterFactory,
private var notifier: PrintNotifier,
private var queue: PrintQueue)
which causes tests to become bloating with a lot of code due the number of dependencies. This is a smell and need to be considered split of them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I make the repository depend on the listener interface or should I scrap it and write a hard-coded variant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you elaborate "write a hard-coded variant?"
Some snippet will be helpful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently I only have one implementation of the listener that waits for a second and prints to the console which I used for testing.
class ConsolePrintingListener : PrintingListener {
override fun notifyPrinted(printResponse: PrintReceiptResponse) {
Thread.sleep(1000)
println(printResponse)
}
}
My question is if I should pass the repository this interface as a dependency, or if I should have something like a logger that logs when finishReceipt or failReceipt are called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread.sleep in testing is not a good idea.
Actually both the Repository and ConsolePrintingListener could implement PrintingListener interface. The persistence will record the result whether the console will print it to the console and this class will replace 2 dependencies with one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something like this?
class PersistentReceiptRepository @Inject constructor(private val database: Provider<MongoDatabase>)
: ReceiptRepository, PrintingListener {
override fun notifyPrinted(printResponse: PrintReceiptResponse) {
val logger = LoggerFactory.getLogger(PrintingListener::class.java)
logger.info("Receipt was printed")
logger.info(printResponse.warnings.toString())
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, just change the name from notifyPrinted to something like: onReceiptPrinted
as listener is a class which receive events and not direct orders like "notifyPrinted".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, understood.
*/ | ||
class InMemoryPrintQueue : PrintQueue { | ||
|
||
private val queue = BlockingArrayQueue<PrintReceiptRequest>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ArrayBlockingQueue is java equivalent which is better to be used as this one is related to jetty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My mistake, changed to java blocking queue.
queue.queue(PrintReceiptRequest(dto.receipt, dto.sourceIp, dto.fiscal)) | ||
|
||
logger.info("Receipt queued for printing") | ||
Reply.with(receiptId).ok() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
status 202 is better to be returned as it will indicate that background processing will be performed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to return status 202.
* | ||
* @author Tsvetozar Bonev ([email protected]) | ||
*/ | ||
interface ReceiptPrintWorker { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure that you need this interface at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but if I remove this interface from the background thread and leave it as a plain AbstractExecutionThreadSerivce, how would I go about binding it's injections with Guice?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you need such binding at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
btw, you can bind and classes too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do not, my mistake, will remove the interface.
a3e83f8
to
1aa75fe
Compare
Updated Swagger with new v2 api endpoints |
Added #66 to clarify changes to persistence in the PR. |
c9cebd5
to
c6386da
Compare
b4ff0e5
to
808872f
Compare
Added endpoint to retrieve the last receipts along with their request id and printing status by their creation date and with a limit argument. |
receiptList.add(ReceiptWithStatusDTO(it.value, it.key, Gson().toJson(receipts[it.key]!!))) | ||
} | ||
|
||
return Reply.with(receiptList).ok() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reply.with(receiptList).as
(GsonTransport::class.java).ok()
} | ||
} | ||
|
||
internal data class ReceiptWithStatusDTO(val status: String, val requestId: String, val receiptJson: String) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggest
ReceiptDTO(val id:String, val amount:String, val requestId: String, val status: String, val isFiscal:Boolean, val amount:Double, val referent...)
} | ||
|
||
internal data class ReceiptWithStatusDTO(val status: String, val requestId: String, val receiptJson: String) | ||
internal data class ReceiptDTO(val sourceIp: String = "", val operatorId: String = "", val fiscal: Boolean = false, val receipt: Receipt = Receipt.newReceipt().build()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tI think that it is better if this dto is PrintReceiptRequest(...)
it look like you are using the domain object as a dto :
Receipt = Receipt.newReceipt().build()
you should create separate dto object that is used only for parsing the request , then you will have to make transformation from dto to the domain
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented a ReceiptWithStatus object that is now stored in persistence, containing information about it's printing status, fiscal status, request id, source ip and its receipt object.
I don't think using the receipt domain object directly here will cause a problem, as the request contains a fully-fledged receipt object within itself already.
3a0a1f7
to
8dc026c
Compare
@@ -1,5 +1,7 @@ | |||
package com.clouway.pos.print.adapter.db | |||
|
|||
import com.clouway.pos.print.core.PrintingListener |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused import
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed the unused import.
if (receipts() | ||
.find(and( | ||
eq("receipt.receiptId", receipt.receiptId), | ||
eq("isFiscal", receiptRequest.isFiscal))).firstOrNull() != null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of .firstOrNull() != null
you could use .any()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched over to using any().
eq("isFiscal", receiptRequest.isFiscal))).firstOrNull() != null) | ||
throw ReceiptAlreadyRegisteredException() | ||
|
||
val requestId = UUID.randomUUID().toString() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is better for me to use an interface for generating ids, this way the testing would be easier and also if you decide to not using UUID
you will make changes only in one place
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created an IdGenerator intreface and injected a SimpleUUIDGenerator to the repository.
*/ | ||
class ReceiptPrintingListener : PrintingListener { | ||
override fun onPrinted(receipt: Receipt, printStatus: PrintStatus) { | ||
val logger = LoggerFactory.getLogger(PrintingListener::class.java) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better if you bring the initialization out of the function
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Must've overlooked this, moved it into the class.
companion object { | ||
@ClassRule | ||
@JvmField | ||
val dataStoreRule = DatastoreRule() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you use @Rule
instead of @ClassRule
there is no need to make dataStoreRule
static
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Making the annotation @rule throws a null pointer and the only way I've managed to get this rule to work is by making it a static ClassRule.
8dc026c
to
e171f1d
Compare
e171f1d
to
b74a569
Compare
Added a receipt repository in mongodb and an async printing service, utilizing the mongo capped collection and a background thread to print receipts after they have been persisted.