Use Xray sdk in Lambda to send segments over UDP - amazon-web-services

I am using Lambda. I want to send a subsegment to Xray with an custom end_time. Xray is enabled in my Lambda.
When I use the aws-xray-sdk-core and addNewSubsegment('postgres') I do not find the possibility to add an end_time. It looks like the end_time is being set when you close() the Segment.
To try and solve this limitation I base myself on the following to send a custom segment to the Xray Daemon using UDP.
Use UDP to send segment to XRay
Below code is not sending a SubSegment to Xray. I am not receiving any errors when sending the segment with client.send(...).
Does someone knows more about this limitation of setting an custom end_time/ knows if it's possible with UDP inside a Lambda?
import AWSXRay from 'aws-xray-sdk-core'
const traceDocument = {
trace_id,
id: generateRandomHex(16),
name: 'postgres',
start_time,
end_time,
type: 'subsegment',
parent_id,
sql: { sanitized_query: query },
}
const client = createSocket('udp4')
const udpSegment = `{"format": "json", "version": 1}\n${traceDocument}`
client.send(udpSegment, 2000, '127.0.0.1', (e, b) => {
console.log(e, b)
})

Managed to find out the solution myself
used the X-Ray SDK with a combination of
addAttribute('in_progress', false) and
streamSubsegments() to send the subsegments to X-Ray
export const onQueryEvent = async (e) => {
try {
const segment = AWSXRay.getSegment()
if (segment) {
const paramsArr = JSON.parse(e.params)
const query = getQueryWithParams(e.query, paramsArr) // X-Ray wants the time in seconds -> ms * 1e-3
const start_time = e.timestamp.valueOf() * 1e-3
const end_time = (e.timestamp.valueOf() + e.duration) * 1e-3
// Add a new Subsegment to parent Segment
const subSegment = segment.addNewSubsegment('postgres') // Add data to the segment
subSegment.addSqlData({ sanitized_query: query })
subSegment.addAttribute('start_time', start_time)
subSegment.addAttribute('end_time', end_time) // Set in_progress to false so subSegment
// will be send to xray on streamSubsegments()
subSegment.addAttribute('in_progress', false)
subSegment.streamSubsegments()
}
} catch (e) {
console.log(e)
}
}

Related

NOT_FOUND(5): Instance Unavailable. HTTP status code 404

I got this error when task is trying processing.
This is my nodejs code
async function quickstart(message : any) {
// TODO(developer): Uncomment these lines and replace with your values.
const project = "";//projectid
const queue = "";//queuename
const location = "";//region
const payload = JSON.stringify({
id: message.id,
data: message.data,
attributes: message.attributes,
});
const inSeconds = 180;
// Construct the fully qualified queue name.
const parent = client.queuePath(project, location, queue);
const task = {
appEngineHttpRequest: {
headers: {"Content-type": "application/json"},
httpMethod: protos.google.cloud.tasks.v2.HttpMethod.POST,
relativeUri: "/api/download",
body: "",
},
scheduleTime: {},
};
if (payload) {
task.appEngineHttpRequest.body = Buffer.from(payload).toString("base64");
}
if (inSeconds) {
task.scheduleTime = {
seconds: inSeconds + Date.now() / 1000,
};
}
const request = {
parent: parent,
task: task,
};
console.log("Sending task:");
console.log(task);
// Send create task request.
const [response] = await client.createTask(request);
console.log(`Created task ${response.name}`);
console.log("Created task");
return true;
}
The task is created without issue. However, it didnt trigger my cloud function and I got 404 or unhandled exception in my cloud logs. I have no idea whats going wrong.
I also did test with gcloud cli without the issue. Gcloud cli able to trigger my cloud function based on provided url.

Pubsub push subscription not acknowledging messages

This is my setup.
Subscription A is a push subscription that POSTs messages to a cloud Run deployment.
That deployment exposes an HTTP endpoint, processes the message, posts the result to Topic B, and responds 200 to subscription A's POST request. The whole process takes ~1.5 seconds.
Therefore, for every message in subscription A, I should end up with 1 message in Topic B.
This is how my code looks like
My app started an Express server
const express = require('express');
const bodyParser = require('body-parser');
const _ = require('lodash');
const startBrowser = require('./startBrowser');
const tab = require('./tab');
const createMessage = require('./publishMessage');
const domain = 'https://example.com';
require('dotenv').config();
const app = express();
app.use(bodyParser.json());
const port = process.env.PORT || 8080;
app.listen(port, async () => {
console.log('Listening on port', port);
});
The endpoint where all the magic happens
app.post('/', async (req, res) => {
// Define the success and fail functions, that respond status 200 and 500 respectively
const failed = () => res.status(500).send();
const completed = async () => {
const response = await res.status(200).send();
if (response && res.writableEnded) {
console.log('successfully responded 200');
}
};
//Process the data coming from Subscription A
let pubsubMessage = decodeBase64Json(req.body.message.data);
let parsed = await processor(pubsubMessage);
//Post the processed data to topic B
let messageId = await postParsedData(parsed);
if (messageId) {
// ACK the message once the data has been processed and posted to topic B.
completed();
} else {
console.log('Didnt get a message id');
// failed();
}
});
//define the functions that post data to Topic B
const postParsedData = async (parsed) => {
if (!_.isEmpty(parsed)) {
const topicName = 'topic-B';
const messageIdInternal = await createMessage(parsed, topicName);
};
return messageId;
} else {
console.log('Parsed is Empty');
return null;
}
};
function decodeBase64Json(data) {
return JSON.parse(Buffer.from(data, 'base64').toString());
}
Execution time takes about ~1.5 seconds and I can see the successful responses logged on Cloud run every ~1.5seconds. That adds up to about ~2400messages/hour (per cloud run instance).
Topic B is getting new messages at ~2400messages/hour, Subscription A's acknowledgement rate is ~200messages/hour, which leads to the messages being re-delivered many times.
Subscription A's Acknowledgement deadline is 600 seconds.
The request timeout period in Cloud run is 300 seconds.
I've tried ACKing messages before they're published to topic-B or even before parsing, but I'm getting the same result.
Edit: added screenshot of the pending messages and processed messages. Many more messages processed than ACKed pending messages. Should be 1:1
Thanks for your help
Solution This error could not be reproduced by GCP support. It didn't happen with large amounts of Cloud Run VMs. The solution is just to increase the number of worker instances
You need to await your complete(); function call. like this
....
if (messageId) {
// ACK the message once the data has been processed and posted to topic B.
await completed();
} else {
console.log('Didnt get a message id');
// failed();
}

Self invoking lambda invocation timing out

We're trying to develop a self-invoking lambda to process S3 files in chunks. The lambda role has the policies needed for the invocation attached.
Here's the code for the self-invoking lambda:
export const processFileHandler: Handler = async (
event: S3CreateEvent,
context: Context,
callback: Callback,
) => {
let bucket = loGet(event, 'Records[0].s3.bucket.name');
let key = loGet(event, 'Records[0].s3.object.key');
let totalFileSize = loGet(event, 'Records[0].s3.object.size');
const lastPosition = loGet(event, 'position', 0);
const nextRange = getNextSizeRange(lastPosition, totalFileSize);
context.callbackWaitsForEmptyEventLoop = false;
let data = await loadDataFromS3ByRange(bucket, key, nextRange);
await database.connect();
log.debug(`Successfully connected to the database`);
const docs = await getParsedDocs(data, lastPosition);
log.debug(`upserting ${docs.length} records to database`);
if (docs.length) {
try {
// upserting logic
log.debug(`total documents added: ${await docs.length}`);
} catch (err) {
await recurse(nextRange.end, event, context);
log.debug(`error inserting docs: ${JSON.stringify(err)}`);
}
}
if (nextRange.end < totalFileSize) {
log.debug(`Last ${context.getRemainingTimeInMillis()} milliseconds left`);
if (context.getRemainingTimeInMillis() < 10 * 10 * 10 * 6) {
log.debug(`Less than 6000 milliseconds left`);
log.debug(`Invoking next iteration`);
await recurse(nextRange.end, event, context);
callback(null, {
message: `Lambda timed out processing file, please continue from LAST_POSITION: ${nextRange.start}`,
});
}
} else {
callback(null, { message: `Successfully completed the chunk processing task` });
}
};
Where recurse is an invocation call to the same lambda. Rest of the things work as expected it just times out whenever the call stack comes on this invocation request:
const recurse = async (position: number, event: S3CreateEvent, context: Context) => {
let newEvent = Object.assign(event, { position });
let request = {
FunctionName: context.invokedFunctionArn,
InvocationType: 'Event',
Payload: JSON.stringify(newEvent),
};
let resp = await lambda.invoke(request).promise();
console.log('Invocation complete', resp);
return resp;
};
This is the stack trace logged to CloudWatch:
{
"errorMessage": "connect ETIMEDOUT 63.32.72.196:443",
"errorType": "NetworkingError",
"stackTrace": [
"Object._errnoException (util.js:1022:11)",
"_exceptionWithHostPort (util.js:1044:20)",
"TCPConnectWrap.afterConnect [as oncomplete] (net.js:1198:14)"
]
}
Not a good idea to create a self-invoking lambda function. In case of an error (could also be a bad handler call on AWS side) a lambda function might re-run several times. Very hard to monitor and debug.
I would suggest using Step Functions. I believe this tutorial can help Iterating a Loop Using Lambda
From the top of my head, if you prefer not dealing with Step Functions, you could create a Lambda trigger for an SQS queue. Then you pass a message to the queue if you want to run the lambda function another time.

Cloud Function (trigger by HTTP) that would publish a message to PubSub

I am trying to create HTTP API in Cloud Function - that eventually published a message t PubSub. Understood, that there is PubSub REST API - but it enforced me to set up the authentication (in client side) - that I would like to skip and move it to the server side.
Below code is deployed as Google Cloud Function with this command gcloud functions deploy helloGET --runtime nodejs8 --trigger-http
But while tested in browser, it is errored out Error: could not handle the request
Any suggestion is appreciated, thanks!
"use strict";
// [START functions_pubsub_setup]
const { PubSub } = require("#google-cloud/pubsub");
// Instantiates a client
const pubsub = new PubSub();
// [END functions_pubsub_setup]
const Buffer = require("safe-buffer").Buffer;
exports.helloGET = (req, res) => {
const topic = pubsub.topic("projects/myproject/topics/openit");
const message = {
data: {
message: "req.body.message"
}
};
// Publishes a message
res.send(
topic
.publish(message)
.then(() => res.status(200).send("Message published."))
.catch(err => {
err = `Catch block ... ${err}`;
console.error(err);
res.status(500).send(err);
return Promise.reject(err);
})
);
};
Below code will work. But it will take around 30 seconds or plus for the subscriber to receive the event - it is way too slow for my used case :S
"use strict";
const { PubSub } = require("#google-cloud/pubsub");
const pubsub = new PubSub();
const Buffer = require("safe-buffer").Buffer;
exports.helloGET = async (req, res) => {
var toPublish = `hello ${Date.now()}`;
publishMessage("_REPLACE_WITH_TOPIC_NAME_", toPublish);
res.send(`Published ${toPublish}`);
};
async function publishMessage(topicName, data) {
console.log("=> publishMessage, data = ", data);
const dataBuffer = Buffer.from(data);
const topic = pubsub.topic(topicName);
const publisher = topic.publisher();
publisher.publish(dataBuffer, { a: "XYZ" }, function() {
console.log("Published eventually ...");
});
}

DB Connection Unavailable Lambda with Cloud Watch Event Rules

I'm having issues connecting to mongodb when I am using a cloud watch event rule to trigger lambdas to keep them warm and also the same issue when I tried using serverless-plugin-warmup. Anyone have and ideas as to why this would be happening? Also I whitelist IP's for my database and use an Elastic IP for my lambda functions. Could the cloud watch event rules be causing the lambdas to use a different IP?
{"error":{"name":"MongoError","message":"no connection available"}}
I wrap my functions with the following to make sure the database is connected before running code
const mongoose = require('mongoose');
mongoose.Promise = global.Promise;
let cachedDB = null;
module.exports = fn => (...args) => {
const [, context] = args;
context.callbackWaitsForEmptyEventLoop = false;
if (cachedDB && cachedDB.readyState != 0 && cachedDB.readyState != 3) {
fn(...args);
} else {
mongoose.connect(process.env.MONGO_URI);
mongoose.connection.on('error', err => {
console.log('Connection Error');
console.log(err);
});
mongoose.connection.once('open', () => {
cachedDB = mongoose.connection;
fn(...args);
});
}
};
You need to handle warmup events as a special case in your handler. Basically, your handler should return right away when it is invoked via a warmup event.
In the example given in serverless-warmup-plugin, you can do it this way,
module.exports.lambdaToWarm = function(event, context, callback) {
// Immediate response for WarmUP plugin
if (event.source === 'serverless-plugin-warmup') {
console.log('WarmUP - Lambda is warm!')
return callback(null, 'Lambda is warm!')
}
// add lambda logic after
}
Notice that there is an if statement at the beginning to check if it's a warmup event. If it is, return successfully right away.
This check should be at the beginning so that it doesn't have to connect to MongoDB at all.