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

pos-print: add async receipt printing #65

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

tsbonev
Copy link

@tsbonev tsbonev commented Sep 19, 2018

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.

@@ -39,6 +40,10 @@ public static void main(String[] args) {
new PersistentModule(client, commandCLI.dbName())
);

BackgroundReceiptPrinter backgroundPrinter = injector.getInstance(BackgroundReceiptPrinter.class);
Copy link
Member

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.

Copy link
Author

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>)
Copy link
Member

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.

Copy link
Author

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()
Copy link
Member

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.

Copy link
Author

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.

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#19

Copy link
Member

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.

Copy link
Contributor

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

Copy link
Member

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.

Copy link
Contributor

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)
Copy link
Member

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.

Copy link
Author

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)
Copy link
Member

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.

Copy link
Author

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 {
Copy link
Contributor

@mlesikov mlesikov Sep 19, 2018

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

Copy link
Author

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.

@tsbonev tsbonev force-pushed the async_print_service branch 7 times, most recently from f4b2d07 to 8266969 Compare September 20, 2018 09:08
@tsbonev
Copy link
Author

tsbonev commented Sep 20, 2018

PR is up to date with requested changes.

var cleaner = DatastoreCleaner(dataStoreRule.db())


private fun Mockery.expecting(block: Expectations.() -> Unit) {
Copy link
Member

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.

Copy link
Author

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 {
Copy link
Member

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)

Copy link
Author

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
Copy link
Member

@mgenov mgenov Sep 20, 2018

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--

Copy link
Author

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);
Copy link
Member

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.

Copy link
Author

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(() -> {
Copy link
Member

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.

Copy link
Author

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()
Copy link
Member

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.

Copy link
Author

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)
Copy link
Member

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

Copy link
Author

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)
Copy link
Member

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.

Copy link
Author

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.

Copy link
Member

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.

Copy link
Author

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?

Copy link
Member

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.

Copy link
Author

@tsbonev tsbonev Sep 20, 2018

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.

Copy link
Member

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.

Copy link
Author

@tsbonev tsbonev Sep 20, 2018

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())
  }

Copy link
Member

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".

Copy link
Author

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>()
Copy link
Member

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.

Copy link
Author

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()
Copy link
Member

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.

https://httpstatuses.com/202

Copy link
Author

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 {
Copy link
Member

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.

Copy link
Author

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?

Copy link
Member

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?

Copy link
Member

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.

Copy link
Author

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.

@tsbonev tsbonev force-pushed the async_print_service branch 3 times, most recently from a3e83f8 to 1aa75fe Compare September 21, 2018 06:50
@tsbonev
Copy link
Author

tsbonev commented Sep 21, 2018

Updated Swagger with new v2 api endpoints

@tsbonev
Copy link
Author

tsbonev commented Sep 28, 2018

Added #66 to clarify changes to persistence in the PR.
Implementation is WIP.

@tsbonev tsbonev force-pushed the async_print_service branch 10 times, most recently from c9cebd5 to c6386da Compare October 3, 2018 10:09
@tsbonev tsbonev force-pushed the async_print_service branch 4 times, most recently from b4ff0e5 to 808872f Compare October 12, 2018 13:23
@tsbonev
Copy link
Author

tsbonev commented Oct 12, 2018

Added endpoint to retrieve the last receipts along with their request id and printing status by their creation date and with a limit argument.
Added endpoint to requeue a receipt by request id.

receiptList.add(ReceiptWithStatusDTO(it.value, it.key, Gson().toJson(receipts[it.key]!!)))
}

return Reply.with(receiptList).ok()
Copy link
Contributor

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)
Copy link
Contributor

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())
Copy link
Contributor

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

Copy link
Author

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.

@tsbonev tsbonev force-pushed the async_print_service branch 6 times, most recently from 3a0a1f7 to 8dc026c Compare October 15, 2018 12:59
@@ -1,5 +1,7 @@
package com.clouway.pos.print.adapter.db

import com.clouway.pos.print.core.PrintingListener

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import

Copy link
Author

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)

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()

Copy link
Author

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()

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

Copy link
Author

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)

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

Copy link
Author

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()

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

Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants