I'm not familiar enough with node.js or lambda to see an obvious solution to a dilemma I have. I'm writing some utilities on lambda to manipulate images in an S3 bucket and make them accessible via the GatewayAPI to rest calls.
BACKGROUND DETAILS:
One of the utilities I have retrieves the headObject information such as the mtime, size and metadata. The images themselves will likely be coming in from various means and I won't always have control over adding metadata to them when they arrive/are-created. But I don't really need it until it's necessary to view details about the image from a web interface. And when I do that, I use a thumbnail instead so I created a lambda create-event triggered script (and also have a fall back variation of it via the gatewayAPI) that will create a thumbnail (either when the image is first uploaded to S3 or whenever I make the gateway CreateThumbbnail call) at which time it adds metadata to the thumbnail for the image with things like the original image mimetype, pixel width and height.
What I would like to be able to do, is to create a 'GetObjectInfo' that firsts pulls the headObject data, then checks to see if the bucket specified is or is not the bucket with the associated thumbnail files. (e.g. if it is or is not a thumbnail object) If it is 'not' a thumbnail, I want to then go retrieve -- or at least attempt to retrieve -- the headObject for the associated thumbnail file and attach the thumbnail file's metadata (if the thumbnail exists) onto the data from the original head request before returning the information.
The problem is, that when I set up an async callback scheme, the first headObject request completes, the second never seems to get out of the starting gate.
The method in my class is:
getHeadObject(bucket,object,callback) {
console.log(bucket, "CLASS-head#1")
this.s3.headObject({"Bucket":bucket,"Key":object}, function(err,data){
console.log(bucket, "CLASS-head#2")
callback(err,data)
})
}
getObjectInfo(bucket,object,callback) {
let scope = this
console.log(bucket,"CLASS-object#1")
this.getHeadObject(bucket,object,function(err,data) {
console.log(bucket,"CLASS-object#2")
if(err)
callback(err,data)
else
callback(null,data)
})
}
The lambda code that calls it recursively is:
var cInst = new myClass()
cInst.getObjectInfo(srcBucket,filePath,function(err,data) {
if(data.status == 1) { // if parent request success
// if parent is not thumbnail
if(srcBucket != THUMB_BUCKET) { // see if a thumbnail exists
let thumbPath = myClass.getThumbPath(srcBucket,userId,directory,targetObject)
console.log('---- thumbPath', thumbPath)
cInst.getObjectInfo(THUMB_BUCKET,thumbPath, function(err,thumbData) {
console.log("thumbData #1",thumbData)
if(thumbData.status == 1) { // thumbnail exists
console.log("thumbData")
}
})
}
context.succeed(myClass.createResponse(1, data, api))
} else {
context.fail(myClass.createResponse(data.status, data, api))
}
})
First call on the parent is see
{bucket} "CLASS-object#1"
{bucket} "CLASS-head#1"
{bucket} "CLASS-head#2"
{bucket} "CLASS-object#2"
on the second I only see:
image-thumbnails "CLASS-object#1"
image-thumbnails "CLASS-head#1"
(getThumbPath is just a static utility function that builds the thumbnail path based on the parameters related to the original file. It is already tested as working and produces something like {original-bucket-name}/{userid}/{subdirectory}/{file-basename_150x150.jpg} for any given image - I confirmed that in this instance, the thumbnail exists and matches the path returned by getThumbPath and the acl appears to have permission to read the bucket and the object)
UPDATE: More weirdness
I tried setting the permissions to publicly readable on the thumbnail and it worked. So I started messing with the acl. For the time being since I am still testing, I just gave the role for the scripts full S3 permissions.
But I noticed now that it's working and not working intermittently. One time it completes, the next time it doesn't. WTF is going on here?
I would bet that this is the most common problem that people see when using Node.js with Lambda.
When a Node.js Lambda reaches the end of the main thread, it ends all other threads. When it reaches the end of the handler, it stops all concurrent promises or async calls that are running.
To make sure that the lambda does not prematurely terminate those threads, wait until those promises are complete by using await.
In your case, the following will work: wrap any async calls in a promise and then await them.
await new Promise(async (resolve, reject) => {
cInst.getObjectInfo(srcBucket,filePath,function(err,data) {
if(data.status == 1) {
if(srcBucket != THUMB_BUCKET) {
...
...
await new Promise((resolve2, reject2) => {
cInst.getObjectInfo(THUMB_BUCKET,thumbPath, function(err,thumbData) {
...
...
resolve2();
})
})
}
context.succeed(myClass.createResponse(1, data, api))
resolve();
} else {
context.fail(myClass.createResponse(data.status, data, api))
reject();
}
})
})
Related
I have a Serverless Lambda function that, in response to an S3 s3:ObjectCreated event, tries to check if a separate item exists in an S3 bucket using the following bit of code using the AWS JavaScript SDK:
exports.somethingSomeSomething = async (event) => {
event.Records.forEach(async (record) => {
let tst = await s3.headObject({
Bucket: "mybucket",
Key: "something.gz"
}).promise()
console.log(tst)
})
};
I'm quite rusty with promises in JS, so I'm not sure why this bit of code doesn't work. For reference, it just dies without outputting anything.
However, the following does work:
exports.somethingSomething = async (event) => {
let tst = await s3.headObject({
Bucket: "mybucket",
Key: "something.gz"
}).promise()
console.log(tst)
console.log("RED")
};
How can I get the initial bit of code working, and what am I doing wrong?
It's because your code is async, but the function passed to your forEach loop is also async, so you have an async function invoking another chunk of async code, therefore you lose control of the flow. Whatever is inside forEach will run (although anything after forEach will run before whatever is inside forEach), but it will execute asynchronously and you are unable to keep track of its execution.
But if the code, as I said, will run, why don't you see the results?
Well, that's because Lambda will terminate before that code has the chance to execute. If you run the same piece of code locally, you'll see it will run just fine, but since the original code runs on top of Lambda, you don't have control when it terminates.
You have two options here:
The easiest is to grab the first item in the Records array because s3 events send one and only one event per invocation. The reason it is an array is because the way AWS works (a common interface for all events). Anyways, your forEach is not using anything of the Record object, but still if you wanted to use any properties of it, simply reference the 0th position, like so:
exports.somethingSomeSomething = async (event) => {
const record = event.Records[0]
//do something with record
const tst = await s3.headObject({
Bucket: "mybucket",
Key: "something.gz"
}).promise()
console.log(tst)
};
If you still want to use a for loop to iterate through the records (although, again, unnecessary for s3 events), use a for of loop instead:
exports.somethingSomeSomething = async (event) => {
for (const record of event.Records) {
// do something with record
const tst = await s3.headObject({
Bucket: "mybucket",
Key: "something.gz"
}).promise()
console.log(tst)
}
};
Since for of is just a regular loop, it will use the async from the function it's being executed on, so await is perfectly valid inside it.
More on async/await and for..of
I'd like to use a Method defined in the Mongoose Model after saving the retrieved Object to a Session. Its not working though. Is it normal that these methods get lost after storing it to the session?
Calling Method from Mongoose Model works fine:
Puppies.findOne({_id:123}).then(puppy => puppy.bark()) // WOOF WOOF
Storing Model in Session and calling method fails:
// First Request
Puppies.findOne({_id:123}).then(puppy => {
req.session.puppy = puppy
})
// Second Request somewhere else in the app
app.use(function(req,res,next){
req.session.puppy.bark() // req.session.puppy.bark is not a function
})
I've got the exact issue, but I believe what happens is that when you're storing the variable in session, it's being toObject()'d, causing it to become a simple JavaScript object, instead of remaining as an instance of Model. I've used Model.hydrate as a means of recreating this Model instance.
app.use(function(req,res,next){
let puppyModel = mongoose.model("puppy");
let puppy = puppyModel.hydrate(req.session.puppy);
puppy.bark() // Awooo
});
This essentially is creating a new Model and then filling it with all the relevant information so it acts a clone.
Because it is needing all the relevant information to make an update (including _id if you have it), I believe you may need to extend the toObject function to return getters/virtuals:
puppySchema.set('toObject', { getters: true, virtuals: true });
Else, when it attempts to save, and it's missing the _id field, it won't be able to save it.
I do hope someone else can provide a nicer method of doing this and/or explain why when storing it it has to be converted to an object and can't remain as an instance of Model.
I think what Ciaran Blewitt said was correct. Finally worked around it by just using mongoose statics:
puppy.model.js
schema.statics.bark(puppy) {
console.log(puppy.sound)
}
Storing Model in Session and getting desired effect via static:
// First Request, storing Puppy in Session
Puppy.findOne({_id:123}).then(puppy => {
req.session.puppy = puppy
})
// Second Request somewhere else in the app
app.use(function(req,res,next){
Puppy.bark(req.session.puppy) // WOOF WOOF
})
I am currently stuck with an issue in my MVC 4 application. I have private variable in controller, that holds instance of a simple class:
private InstallationStatus status = null;
When data get submitted on a view, it gets filled like this:
InstallationStatus installStatus = Install();
if (installStatus != null)
{
status = installStatus;
TempData["installPercent"] = 0;
return View("InstallationProgress", status);
}
This part works as intended, variable is set to the instance as it should be.
After that view periodically checks another variable (using ajax):
<script type="text/javascript">
$(document).ready(function () {
var progress = 0;
$("div.status-message").text("Progress: " + progress + "%");
var statusUpdate = setInterval(function () {
$.ajax({
type: 'GET',
url: "/AppStart/GetInstallProgress",
datatype: "application/html; charset=utf-8",
success: function (data) {
progress = parseInt(data);
if (progress >= 100) {
clearInterval(statusUpdate);
var data = $(this).serialize();
$.ajax({
type: 'POST',
url: "#Url.Action("CompletedStatus", "AppStart")",
success: function () {
window.location = "/Login/Login"
}
});
}
$("div.status-message").text("Progress: " + progress + "%");
}
});
}, 2000);
});
</script>
When it calls "CompletedStatus" action on the controller, variable "status" on the controller is null (the instance set previously is not there?
How do I ensure that its value will persist? It seems to me like whole instance of controller gets lost, but that doesnt really matter to me - the source for "status" is webservice and once I get the instance of InstallationStatus, I cant get it again - I need to keep it.
I tried using TempData to store it but since there can be more than one step between storing it and retrieving it TempData proved unreliable.
The final process is:
Request installation status and navigate to view for installation progress (status will be received when progress will finish).
navigate to view where I will by updating installation progress
using javascript whenever I get callback from server with info about
progress
when installation finishes (status is returned) pass that status to
another view
In the example above I have some dummy code-behind, so the status is returned immediately, but that has no impact on the problem I have.
Currently I can do 1 and 2 and I can call the final view, but I cant pass the status in there because I dont have it on controller anymore and TempData are not reliable for this (sometimes it is still there, sometimes it is not).
Any help will be greatly appreciated.
When it calls "CompletedStatus" action on the controller, variable
"status" on the controller is null (the instance set previously is not
there?
How do I ensure that its value will persist?
private InstallationStatus status = null;
It won't unless it's a static value and that would be a very bad thing to do. Remember that variable values (private members' values) are only scoped within the http request. If you do another request then that's a totally whole new scope for your private variables.
I tried using TempData to store it but since there can be more than
one step between storing it and retrieving it TempData proved
unreliable.
That's because TempData will not have the value you expect it to have once you do another request. One good example of using this TempData is when you want to pass/move some values between a POST and GET, that is when you do a POST and do a redirect. TempData does not fit your case.
Now for a possible solution to your scenario, a good question is: is the installation process called once? Is it unique per user? If it is, which I highly suspect it is, then you need to uniquely identify each request. You can simply use a GUID to identify each request. Save that into your database (better than saving in a session) along with some other information like the status of the installation. Pass that guid back to your client and let them pass it back to the controller and retrieve an update on the status of the installation.
I am wanting to use a CredentialPicker to prompt for a username and password. When I configure an instance of this class, I can set CredentialPickerOptions.PreviousCredential to a value previously obtained by CredentialPickerResults.Credential. I believe this causes the dialog to prepopulate the credentials.
However, persisting this value appears to be non-trivial; it's an IBuffer, whose members don't appear to contain the relevant credentials. Programming Windows 8 Apps with HTML, CSS, and JavaScript, page 657, implies that this should be possible:
An IBuffer containing the credential as an opaque byte array. This is what you can
save in your own persistent state if needs be and passed back to the picker at a later time; we’ll
see how shortly.
Unfortunately, the we'll see how shortly appears to only refer to the fact that the value can be passed back from memory into PreviousCredential; I didn't find any mention of how it's persisted.
Also, I want to persist the credentials using the recommended approach, which I believe is to use PasswordVault, however, this appears to only allow me to save the credentials as username and password strings rather than an IBuffer.
Thanks for taking the time to ask, and I certainly agree that I could've been more clear in that part of the book. Admittedly, I spent less time on Chapter 14 than I would have liked, but I'll try to remedy that in the next edition. Feedback like yours is extremely valuable in knowing where I need to make improvements, so I appreciate it.
Anyway, writing a buffer to a file is something that was mentioned back in Chapter 8 (and could've been mentioned again here...page 325, though it doesn't mention IBuffer explicitly). It's a straightforward job using the Windows.Storage.FileIO class as shown below (promise!).
First, a clarification. You have two ways to save the entered credentials. If you want to save the plain-text credentials, then absolutely use the Credential Locker. The benefit here is that those credentials can roam automatically with the user if that roaming passwords is enabled in PC Settings (it is by default). Otherwise, you can save the opaque CredentialPickerResults.credential property directly to a file. It's already encrypted and scrambled, so you don't need to use the credential locker in that case.
Now for saving/loading the credential property, which is an IBuffer. For this you use FileIO.writeBufferAsync to save and FileIO.readBufferAsync to reload.
I modified the Credential Picker sample, scenario 3 to demonstrate this. To save the credential property, I added this code at the end of the completed handler for pickAsync:
//results.credential will be null if the user cancels
if (results.credential != null) {
//Having retrieved a credential, write the opaque buffer to a file
var option = Windows.Storage.CreationCollisionOption.replaceExisting;
Windows.Storage.ApplicationData.current.localFolder.createFileAsync("credbuffer.dat", option).then(function (file) {
return Windows.Storage.FileIO.writeBufferAsync(file, results.credential);
}).done(function () {
//No results for this operation
console.log("credbuffer.dat written.");
}, function (e) {
console.log("Could not create credbuffer.dat file.");
});
}
Then I created a new function to load that credential, if possible. This is called on the Launch button click instead of launchCredPicker:
//In the page ready method:
document.getElementById("button1").addEventListener("click", readPrevCredentialAndLaunch, false);
//Added
function readPrevCredentialAndLaunch() {
Windows.Storage.ApplicationData.current.localFolder.getFileAsync("credbuffer.dat").then(function (file) {
return Windows.Storage.FileIO.readBufferAsync(file);
}).done(function (buffer) {
console.log("Read from credbuffer.dat");
launchCredPicker(buffer);
}, function (e) {
console.log("Could not reopen credbuffer.dat; launching default");
launchCredPicker(null);
});
}
//Modified to take a buffer
function launchCredPicker(prevCredBuffer) {
try {
var options = new Windows.Security.Credentials.UI.CredentialPickerOptions();
//Other options omitted
if (prevCredBuffer != null) {
options.previousCredential = prevCredBuffer;
}
//...
That's it. I put the modified JS sample on http://www.kraigbrockschmidt.com/src/CredentialPickerJS_modified.zip.
.Kraig
Author, Programming Windows 8 Apps in HTML, CSS, and JavaScript (free ebook)
I'm trying to replace an object with a new one and am getting the mentioned exception. I've tried several combination and can't get around it.
I have a Playlist that has Items (the Items has another nested object, but I'm leaving it out to help make my question clearer. The user can change which items are in the playlist.
if (playlistChanged)
{
// remove selectedForRemoval
IEnumerable<PlaylistItemViewModel> nonSelectedItems = selectedDisplayTemplates.Where(pivm => pivm.IsSelectedForRemoval);
foreach (temViewModel ivm in nonSelectedItems)
{
context.DeleteObject(ivm.Model);
}
// clear out and remove old items
foreach (Item item in playlist.PlaylistItems)
{
context.DeleteObject(item);
}
playlist.PlaylistItems.Clear();
// add the selectedItem(s) to the playlist
// these items can be from the Media, or other tables
// so are newly created in code on the client
foreach (ItemViewModel ivm in selectedItems)
{
playlist.Items.Add(ivm.PlaylistItemModel);
context.AddToItems(ivm.PlaylistItemModel);
}
context.BeginSaveChanges(SaveChangesOptions.Batch, new AsyncCallback((iar) =>
{
try
{
// Callback method for the async request, retrieves the status of the requested action
DataServiceResponse response = context.EndSaveChanges(iar);
}
catch (DataServiceRequestException)
{
throw;
}
}), context);
}
Any help is appreciated.
EDIT: I was overriding the Equals and ToString in Playlist partial class. After I removed those, it started working.
I was overriding the Equals and ToString in Playlist partial classes in Silverlight. After I removed those, it started working. I'm going to avoid that from now on with WCF Data Services.
If you fetched the data using a different context from the one you are trying to add/delete with, you will get the exception you posted. Either dispose of the original context you fetched the data with or explicitly call Detach on the item you are calling AddItem/DeleteObject on.