I am looking to queue requests to ExpressJs so that only one request is processed on an endpoint at a time. I have found some examples of this: ExpressJS backend put requests into a queue
It seems though they require a separate function for each endpoint. I am trying to create one function that allows me to pass a queue name, and then stack items in the specified queues.
It is an API listening for requests, and upon receiving one will then execute a request to another online API before relaying the result back to the user through the original express endpoint. Ultimately I will then look to add some very basic caching for each endpoint, just to store a short JSON string for 3 seconds before expiring. That way it returns the cached string within the 3 second limit rather than fetch the data again from online.
Here is as far as I got, I would be curious to hear if there are better ways:
//UI request -> check cache -> return response || call request then return response
// Queue items on endpoint
class QueueUnique {
func;
q;
requestCache = [];
constructor(func) {
this.q = Promise.resolve();
this.func = func;
}
add(request) {
// Fetch all cached items related to the current endpoint queue
const cachedItem = this.requestCache.find(
(itm) => itm.queueName === request.queueName
);
// If the current request is within X seconds of the last successful requesst, return the cache
// otherwise make a new request
if (cachedItem && new Date().getTime() - cachedItem.runtime > 3000) {
console.log(
"Cache is over 3 seconds old. Doing new request. Queue name: " +
request.queueName
// no cahe, forward request:
//seperate this in to function
//res.sendResponse = res.send
// res.send = (body) => {
// request.body = body
// this.updateCache(request);
// res.sendResponse(body)
//}
//next()
);
this.updateCache(request);
} else if (cachedItem) {
console.log("Valid cache, return cache");
// res.send(request.body)
this.updateCache(request);
} else {
console.log("no cache");
//continue as normal as if no cache
// no cahe, forward request: Same as first run
this.addToCache(request);
}
// Do I need to use await before setting datetime?
//then cache
// Set the current time as a value in the item Array
request.runtime = new Date().getTime();
const queuedFunc = this.queue(request);
queuedFunc();
}
addToCache(request) {
// Add the new item to the permanent cache
this.requestCache.push(request);
}
updateCache(request) {
// Update the permanent request cache entry
const arrayIndex = this.requestCache.findIndex(
(itm) => itm.queueName === request.queueName
);
this.requestCache[arrayIndex] = request;
}
queue(item) {
return () => {
this.q = this.q
.then(() => this.func(item))
.catch((err) => {
console.log(err);
});
return this.q;
};
}
}
const response = (item) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("say", item.payload);
resolve();
}, item.delay);
});
};
const queue = [];
function test(bar, payload) {
if (!queue[bar]) {
queue[bar] = new QueueUnique(response);
}
queue[bar].add(payload);
console.log(queue);
return queue;
}
test("te", {
queueName: "ping",
payload: "one",
delay: 3000,
});
test("te", {
queueName: "ping",
payload: "one",
delay: 3000,
});
test("te", {
queueName: "ping",
payload: "one",
delay: 3000,
});
test("te2", {
queueName: "ping",
payload: "two",
delay: 1000,
});
test("te2", {
queueName: "ping",
payload: "two",
delay: 1000,
});
Here is what I have for now. Would be great to hear about improvements or issues. It is converted to Typescript as it is what I ultimately will be using.
import queueCache from './middleware/queueCache'
...
app.locals.cacheTimeout = 3000
app.get('/test', queueCache, (_req, res) => {
res.json({ test: 'test' })
})
app.get('/test1', queueCache, (_req, res) => {
res.json({ test1: 'test1' })
})
queueCache.ts:
import { Request, Response, NextFunction } from 'express'
interface requestData {
queueName?: string
cachedData?: string
runtime?: number
}
const cachedItemList = {} as Array<requestData>
const queue = [] as Array<QueueUnique>
const requestCache = [] as Array<requestData>
// Class to queue items and cache results
class QueueUnique {
func
q: Promise<unknown>
constructor(
func: (req: Request, res: Response, next: NextFunction) => Promise<unknown>
) {
this.func = func
this.q = Promise.resolve()
}
add(req: Request, res: Response, next: NextFunction) {
// Check if the item is already cached
if (checkCache(req, res)) {
return
}
// If not cached, add to queue
const queuedFunc = this.queue(req, res, next)
queuedFunc()
}
queue(req: Request, res: Response, next: NextFunction) {
return () => {
this.q = this.q
.then(() => this.func(req, res, next))
.catch((err) => {
console.log(err)
})
return this.q
}
}
}
const response = (req: Request, res: Response, next: NextFunction) => {
// Do another check to see if item just finished in queue created a useful cache
return new Promise((resolve) => {
if (checkCache(req, res)) {
resolve(true)
return
}
setTimeout(() => {
if (cachedItemList[0].queueName) {
// Got this far, and cache exists so it must be older than set time, starting new request.
// Return response to user
res.sendResponse = res.json
res.json = (body) => {
res.sendResponse(body)
// Find the current item in the request cache
const arrayIndex = requestCache?.findIndex(
(itm) => itm.queueName === req.url
)
// Set the time that the request was stored
requestCache[arrayIndex].runtime = new Date().getTime()
// Store the body of the response in the cache
requestCache[arrayIndex].cachedData = body
return res
}
} else {
// There was no cache
// Return the response to the caller
res.sendResponse = res.json
res.json = (body) => {
res.sendResponse(body)
// Only use cache on GET requests. When not GET, this middleware only acts as a queue.
if (req.method === 'GET') {
// Check if it is already in cache to avoid duplicates
// Overcomes an error: https://github.com/expressjs/express/issues/4826
const arrayIndex = requestCache?.findIndex(
(itm) => itm.queueName === req.url
)
if (arrayIndex === -1) {
requestCache.push({
cachedData: body, // Add the new item to the permanent cache
queueName: req.url, // Add the request URL to the item for later reference
runtime: new Date().getTime() // Add the time the request was made
})
}
}
return res
}
}
next()
resolve(true)
}, 4000)
})
}
function checkCache(req: Request, res: Response) {
// Fetch all cached items related to the current endpoint queue, which is named after the endpoint url
cachedItemList[0] =
requestCache.find((itm) => itm.queueName === req.url) || {}
// If the current request is within X seconds of the last successful requesst, return the cached version
if (
cachedItemList[0].runtime &&
new Date().getTime() - cachedItemList[0].runtime <
req.app.locals.cacheTimeout
) {
// Return the cached item to the user
res.json(cachedItemList[0].cachedData)
return true
} else {
return false
}
}
// Create multipule queues, one for each endpoint
function sortQueues(req: Request, res: Response, next: NextFunction) {
// Use the endpoint name to create a queue within the queues array
if (!queue[req.route.path]) {
queue[req.route.path] = new QueueUnique(response)
}
queue[req.route.path].add(req, res, next)
}
export default sortQueues
Related
I am working in an application : Java Backend and Angular frontend. I am using angular Fromly, data is coming to service, but from the service it is not going to server.
lets share the code snipts:
Service Code:
export class RecommendationRequestService {
readonly ROOT_URL = environment.apiUrl + '/am/v1/recommendation-requests';
constructor(private http: HttpClient, private configService: RecommenderConfigService) {
}
updateData(interviewStatus: InterviewStatusRecommendation): Observable<any> {
console.log(interviewStatus);
return this.http.put<any>(this.ROOT_URL, interviewStatus);
}
}
This line is printing intended data set : console.log(interviewStatus);
The server is running.
The code from where the service is being called :
onSubmit() {
this.model.recommendationRequest.agentInitiationId = this.agentInitiationId;
const subs = this.service.updateData(this.model).subscribe(response => {
console.log('------' + response);
if (response === 'OK') {
this.notify.success('Request Recommendation Update success.');
} else {
this.notify.error('Request Recommendation Update fail.');
}
},
err => {
if (err.error.hasOwnProperty('code') && err.error.code === 1000) {
this.notify.error(CommonEnum.VALIDATION_ERROR);
}
});
subs.unsubscribe();
}
console.log('------' + response); this line should print at least -----, But nothing.
I have checked the network monitor from the browser, no call is going.
What might be the possible issue, any thing from fromly?
You are doing it incorrect as Aldin Bradaric also updated in the comment, as soon as you make the call on the very next moment you are unsubscribing it. This is what you should do :
public subs: [] = [];
onSubmit() {
this.model.recommendationRequest.agentInitiationId = this.agentInitiationId;
const subs = this.service.updateData(this.model).subscribe(response => {
console.log('------' + response);
if (response === 'OK') {
this.notify.success('Request Recommendation Update success.');
} else {
this.notify.error('Request Recommendation Update fail.');
}
},
err => {
if (err.error.hasOwnProperty('code') && err.error.code === 1000) {
this.notify.error(CommonEnum.VALIDATION_ERROR);
}
});
//subs.unsubscribe(); // remove it and add it to the lifecycle hooks
this.subs.push(subs);
}
ngOnDestroy() {
// create an array of subscription
this.subs.forEach(sub => sub.unsubscribe() )
}
I have a requirement where a client calls a post REST endpoint created via akka http. As soon as the request is in the post method, I need to pass the post object to the stream (consisting of source, several flows and sink, etc) and get back the response from the sink so that I can return the response back to the client.
I have been going through some articles and have seen the below code but have a concern that I don't want to materialize the stream for every request. I only want to materialize a stream and keep on passing the elements to that stream.
Below is the high level of what I saw:
val route: Route =
path("dummy path") { p =>
get {
(extract(_.request) & extractMaterializer) { (req, mat) ⇒
**Source.single(req).runWith(sink)(mat)**
complete {
s"<h1>Say hello to akka-http. p=$p</h1>"
}
}
}
}
I was thinking of creating an actor and passing the object to that actor. I can create a source from Source.actorRef and connect several flows with this source. But I am not sure, how to get back the response from the sink. Something like:
val actor: ActorRef = some actor
Source.actorRef(actor).via(flows).to(Sink).run() --> materialized stream
val route: akka.http.scaladsl.server.Route =
path("post" / Segment) { p =>
post {
(extract(_.request) & extractMaterializer) { (req, mat) ⇒
response = actor.ask(message) --> get back the response
complete {
response
}
}
}
}
Or, is there anything else that I can incorporate in my use-case.
I guess what you want is to make the processing of request flow through a stream [materialized only once] and send the response back to the user from the stream. May be a queue source and an Actor in between can do the job
import java.util.concurrent.TimeUnit
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.http.scaladsl.Http
import akka.http.scaladsl.server.Directives.{
get,
onSuccess,
pathEnd,
pathPrefix
}
import akka.pattern.ask
import akka.stream.scaladsl.{Keep, Sink, Source, SourceQueueWithComplete}
import akka.stream.{ActorMaterializer, OverflowStrategy, QueueOfferResult}
import akka.util.Timeout
import akka.http.scaladsl.server.directives.RouteDirectives.complete
import com.typesafe.config.ConfigFactory
import scala.concurrent.ExecutionContext
object TestApp2 extends App {
implicit val actorSystem = ActorSystem("test-system")
implicit val mat = ActorMaterializer()
implicit val ec = mat.executionContext
val streamSource = Source
.queue[(Message, ActorRef)](100, OverflowStrategy.dropNew)
.map { p =>
//do anything here
println("I am processing request")
("It works", p._2)
}
.toMat(Sink.foreach { resp =>
resp._2 ! resp._1
})(Keep.left)
.run()
implicit val timeout = Timeout(
10000,
TimeUnit.MILLISECONDS
)
val internalActor =
actorSystem.actorOf(Props(new InternalActor(streamSource)))
Http(actorSystem)
.bindAndHandle(
getRoutes(internalActor),
"0.0.0.0",
8080
)
def getRoutes(
internalActor: ActorRef
)(implicit mat: ActorMaterializer, ec: ExecutionContext, timeout: Timeout) = {
pathPrefix("healthcheck") {
get {
pathEnd {
val responseReturned = internalActor ? Message()
onSuccess(responseReturned) {
case response: String =>
complete(response)
case _ => complete("error")
}
}
}
}
}
}
case class Message()
class InternalActor(streamSource: SourceQueueWithComplete[(Message, ActorRef)])(
implicit ec: ExecutionContext
) extends Actor {
override def receive: Receive = {
case m: Message =>
val senderRef = sender()
streamSource.offer((m, senderRef)).map {
case QueueOfferResult.Enqueued => // do nothing for success
case QueueOfferResult.Dropped => senderRef ! "error" // return error in case of backpressure
case QueueOfferResult.Failure(ex) => senderRef ! "error" // return error
case QueueOfferResult.QueueClosed => senderRef ! "error" // return error
}
}
}
curl 'http://localhost:8080/healthcheck'
It works
I'm using a file upload example from the following link:
enter link description here
You can see in the example that the server need to return status "progress"
in order to see the progress bar.
What I have in my rest api at the moment:
#POST
#Path("Trip/{tripId}")
#Consumes("multipart/form-data")
#Produces("application/json")
public Response uploadTripVideo(#PathParam("tripId") Integer tripId, MultipartFormDataInput input){
String fileName = "";
Map<String, InputPart> uploadForm = input.getFormData();
InputPart inputPart = uploadForm.get("uploadedFile");
try {
MultivaluedMap<String, String> header = inputPart.getHeaders();
fileName = getFileName(header);
//convert the uploaded file to inputstream
InputStream inputStream = inputPart.getBody(InputStream.class,null);
byte [] bytes = IOUtils.toByteArray(inputStream);
//constructs upload file path
fileName = "C:\\Users\\name\\Documents\\myfolder\\trip\\"+ tripId + "\\video\\" + fileName;
writeFile(bytes,fileName);
} catch (IOException e) {
e.printStackTrace();
}
return Response.status(200)
.entity("uploadFile is called, Uploaded file name : " + fileName).build();
}
here is my service call:
uploadVideo(url: string, file: File): Observable<any> {
let formData = new FormData();
formData.append('uploadedFile', file, file.name);
return this.http.post<any>(this.baseUrl + url, formData, {
reportProgress: true,
observe: 'events'
}).pipe(
map(event => this.getEventMessage(event, formData)),
catchError(this.handleError)
);
}
Any idea how to return a response that should indicate on the progress? The probrem is that the event is not coming when calling the service, here is
the code where I subscribe to the post request:
this.upload.uploadVideo(url, this.videoToUpload)
.subscribe(
(event) => {
console.log(event);
if (event.type === HttpEventType.DownloadProgress) {
console.log("download progress");
}
if (event.type === HttpEventType.Response) {
console.log("donwload completed");
}
this.videoUpload = event;
//console.log("POST call successful value returned in body", val);
},
err => {
this.videoUploadError = err;
//console.log("POST call in error", response);
},
() => {
//console.log("The POST observable is now completed.");
});
What I'm getting is error in the console:
Backend returned code undefined, body was: undefined
UPDATE
I've removed the following code and things start moving:
//.pipe(
// map(event => this.getEventMessage(event, formData)),
// catchError(this.handleError)
// );
You can easily do this by setting the reportProgress flag to true in your POST HttpRequest.
The key here is to create a HttpRequest and pasing it to the HttpClient.request method rather than directly calling the post() method.
Once subscribed to the request, you need to check for the event type as
event.type == HttpEventType.UploadProgress
to perform the logic to show loading percentage as
100 * event.loaded / event.total
and check for the completion as
event.type === HttpEventType.Response
Demo at https://stackblitz.com/edit/angular-http-post-status
Here is my CorsRoute taken directly from the Spark documentation:
class CorsRoute(origin: String, methods: String, headers: String) {
init {
options("/*") { request, response ->
val accessControlRequestHeaders = request.headers("Access-Control-Request-Headers")
if (accessControlRequestHeaders != null) {
response.header("Access-Control-Allow-Headers", accessControlRequestHeaders)
}
val accessControlRequestMethod = request.headers("Access-Control-Request-Method")
if (accessControlRequestMethod != null) {
response.header("Access-Control-Allow-Methods", accessControlRequestMethod)
}
"OK"
}
before { request, response ->
response.header("Access-Control-Allow-Origin", origin)
response.header("Access-Control-Request-Method", methods)
response.header("Access-Control-Allow-Headers", headers)
response.type("application/json")
}
}
}
Here is how I instance the CorsRoute:
abstract class Route {
init {
CorsRoute("*","*","*")
}
}
Here is how I start my routes:
class RestApiRoutes : Route() {
fun init() {
get("/test") {
req, res -> {
val obj = JsonObject().addProperty("foo", "bar")
Gson().toJson(obj).toString()
}
}
}
}
When I head over to http://127.0.0.1:4567/test I receive:
() -> kotlin.String
Why is the page returning this and not my expected JSON response?
Because
{
req, res -> {
val obj = JsonObject().addProperty("foo", "bar")
Gson().toJson(obj).toString()
}
}
is a lambda which returns a lambda without arguments, and not a String. And that inner lambda's toString returns () -> kotlin.String.
Remove the inner {} to fix the problem:
{ req, res ->
val obj = JsonObject().addProperty("foo", "bar")
Gson().toJson(obj).toString()
}
(see https://kotlinlang.org/docs/reference/coding-conventions.html#lambda-formatting for formatting conventions)
Is there any way to execute multiple requests in sequence in Retrofit?
These requests uses same Java interface and differ only by parameters they take which are contained in ArrayList.
For requests A1, A2, A3, A4, A5...... An
Hit A1,
onResponse() of A1 is called
Hit A2,
onResponse() of A2 is called
Hit A3
.
.
.
.
.
.
onResponse() of An is called.
The problem can be easily solved with RxJava.
Assuming you have a retrofit Api class, that returns a Completable:
interface Api {
#GET(...)
fun getUser(id: String): Completable
}
Then you can perform this:
// Create a stream, that emits each item from the list, in this case "param1" continued with "param2" and then "param3"
Observable.fromIterable(listOf("param1", "param2", "param3"))
// we are converting `Observable` stream into `Completable`
// also we perform request here: first time parameter `it` is equal to "param1", so a request is being made with "param1"
// execution will halt here until responce is received. If response is successful, only then a call with second param ("param2") will be executed
// then the same again with "param3"
.flatMapCompletable { api.getUser(it) }
// we want request to happen on a background thread
.subscribeOn(Schedulers.io())
// we want to be notified about completition on UI thread
.observeOn(AndroidSchedulers.mainThread())
// here we'll get notified, that operation has either successfully performed OR failed for some reason (specified by `Throwable it`)
.subscribe({ println("completed") }, { println(it.message) })
If your retrofit API does not return a Completable, then change api.getUser(it) to api.getUser(it).toCompletable().
You can do this easily by using zip function in Rx (For ex: each request in retrofit 2 return Observable<Object>). It will run request sequentially. You can try my codes below:
public Observable buildCombineObserverable() {
List<Observable<Object>> observables = new ArrayList<>();
for (int i = 0; i < number_of_your_request; i++) {
observables.add(your_each_request_with_retrofit);
}
return Observable.zip(observables, new FuncN<Object>() {
#Override
public Object call(Object... args) {
return args;
}
});
}
You can subscribe this Observable and get data from all requests. Zip function will do request sequentially but zip data and return data as Object...
Ok, I don't know about Retrofit, I use loopj's library, but the concept is the same. they both have a method for success and a method for failure. so here is my general suggestion:
ArrayList<MyRequest> requests = new ArrayList<>();
int numberOfRequests = 10;
JSONObject params = null;
try{
params = new JSONObject("{\"key\":\"value\"}");
}catch(JSONException e){
e.printStackTrace();
}
MyRequest firstRequest = new MyRequest();
requests.add(firstRequest);
for(int i = 0; i < numberOfRequests; i++){
MyRequest myRequest = new MyRequest();
requests.get(requests.size() - 1).addNextRequest(myRequest);
myRequest.addPreviousRequest(requests.get(requests.size() - 1));
//don't invoke sendRequest before addNextRequest
requests.get(requests.size() - 1).sendRequest(params, "example.com", App.context);
requests.add(myRequest);
}
requests.get(requests.size() - 1).sendRequest(params, "example.com", App.context);
and the MyRequest class:
import android.content.Context;
import com.loopj.android.http.AsyncHttpClient;
import com.loopj.android.http.AsyncHttpResponseHandler;
import org.json.JSONObject;
import cz.msebera.android.httpclient.Header;
import cz.msebera.android.httpclient.entity.StringEntity;
public class MyRequest{
private Object result, nextRequestsResult;
private MyRequest nextRequest, previousRequest;
public void addNextRequest(MyRequest nextRequest){
this.nextRequest = nextRequest;
}
public void addPreviousRequest(MyRequest previousRequest){
this.previousRequest = previousRequest;
}
public void sendRequest(JSONObject parameters, String url, Context ctx){
AsyncHttpClient mClient = new AsyncHttpClient();
StringEntity entity = new StringEntity(parameters.toString(), "UTF-8");
String contentType = "application/json";
mClient.post(ctx, url, entity, contentType,
new AsyncHttpResponseHandler(){
private void sendResult(Object... results){
MyRequest.this.result = results;
if(previousRequest != null){
if(nextRequest != null){
if( nextRequestsResult != null){
previousRequest.onResult(results, nextRequestsResult);
}else{
//next request's result is not ready yet
//so we don't do anything here. When nextRequestsResult
//gets ready, it will invoke this request's onResult
}
}else {
//nextRequest == null means this the last request
previousRequest.onResult(results);
}
}else{
//previousRequest == null means this is the first request
if(nextRequest != null){
if(nextRequestsResult != null){
previousRequest.onResult(results, nextRequestsResult);
}else{
//next request's result is not ready yet
//so we don't do anything here. When nextRequestsResult
//gets ready, it will invoke this request's onResult
}
}else{
//next request and previous request are null so it means
//this is the only request, so this is the final destination
doFinalJobWithResults(results);
}
}
}
#Override
public void onSuccess(final int statusCode, final Header[] headers,
final byte[] responseBody){
sendResult(responseBody, true, null, false);//whatever
}
#Override
public void onFailure(final int statusCode, final Header[] headers,
final byte[] responseBody,
final Throwable error){
sendResult(responseBody, error);//or just sendResult();
}
});
}
/**
This method should be invoked only by next request
#param nextRequestsResult
results of the next request which this request is expecting.
*/
private void onResult(Object... nextRequestsResult){
this.nextRequestsResult = nextRequestsResult;
//do whatever you want with the result of next requests here
if(previousRequest != null){
if(result != null){
previousRequest.onResult(result, this.nextRequestsResult);
}
}else{
//if it doesn't have previous request then it means this is the first request
//so since this method gets invoked only by next request then it means
//all of the next requests have done their job and this is the final destination
if(nextRequestsResult != null){
if(this.result != null){
doFinalJobWithResults(nextRequestsResult, this.result);
}
}
}
}
private void doFinalJobWithResults(Object... results){
//whatever
}
}
It's a general purpose class, you can send hundreds of network requests simultaneously but their results will be processed in sequence.
This way for example 100 requests will be sent to the server but it takes the time of one request to get all responses of them and process.
I haven't tested this code at all, it may have some bugs and mistakes, I wrote it just for this question only to give an idea.