Thursday, 10 January 2019

Holiday Price Scraper (Serverless Architecture - FaaS) - Part 2


Phase 2


So this article follows on from part one which you can read here: http://dale-dev.blogspot.com/2019/01/holiday-price-scraper-serverless.html, in phase 2 I want to accomplish the following things...

  • The function in AWS is not working when running when triggered by the timer, this needs resolving.
  • The function in AWS is not persisting the history of retrieved prices, I will be creating a DynamoDB DAO to persist this data.
  • Search criteria to be persisted as saved searches rather than hard coded.


AWS Trigger issue


The timer job I set up in AWS which triggers the function fails, the error from the log shows the following...


 REPORT RequestId: db8ec301-0fbe-11e9-902c-ad0f3698404b     Duration: 442.69 ms     Billed Duration: 500 ms Memory Size: 512 MB     Max Memory Used: 98 MB       
 START RequestId: db8ec301-0fbe-11e9-902c-ad0f3698404b Version: $LATEST  
 An error occurred during JSON parsing: java.lang.RuntimeException  
 java.lang.RuntimeException: An error occurred during JSON parsing  
 Caused by: java.io.UncheckedIOException: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token  
      at [Source: lambdainternal.util.NativeMemoryAsInputStream@78b729e6; line: 1, column: 1]  
 Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.lang.String out of START_OBJECT token  
      at [Source: lambdainternal.util.NativeMemoryAsInputStream@78b729e6; line: 1, column: 1]  
      at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)  
      at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:857)  
      at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:62)  
      at com.fasterxml.jackson.databind.deser.std.StringDeserializer.deserialize(StringDeserializer.java:11)  
      at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:1511)  
      at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1102)  
 END RequestId: db8ec301-0fbe-11e9-902c-ad0f3698404b  


From that, it's showing that there is a problem de-serializing the input from the trigger. I didn't actually configure an input, I assumed a null value would be passed and everything would be ok but I assumed wrong (What's that they say about assumptions?).

So going back into AWS, I can see there is an option to configure the input passed from the cloud event so I made the following change





DynamoDB Persistence

Setup


Under DynamoDB, I have selected "Create Table" and enter the following...

Table Name : PriceHistory
Primary key : searchUUID



New DynamoDB DAO


Following this guide: https://www.javaworld.com/article/3248595/application-development/serverless-computing-with-aws-lambda-part-2.amp.html I created the following code

pom.xml

     <dependency>  
       <groupId>com.amazonaws</groupId>  
       <artifactId>aws-java-sdk-dynamodb</artifactId>  
       <version>1.11.475</version>  
     </dependency>  


DynamoDBPriceStoreDao

 package com.daledev.holidaypricescrapper.dao.aws;  
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;  
 import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;  
 import com.amazonaws.services.dynamodbv2.document.*;  
 import com.daledev.holidaypricescrapper.dao.PriceStoreDao;  
 import com.daledev.holidaypricescrapper.domain.PriceHistory;  
 import org.slf4j.Logger;  
 import org.slf4j.LoggerFactory;  
 import org.springframework.stereotype.Repository;  
 import java.util.ArrayList;  
 import java.util.List;  
 import java.util.UUID;  
 /**  
  * @author dale.ellis  
  * @since 04/01/2019  
  */  
 @Repository  
 public class DynamoDBPriceStoreDao implements PriceStoreDao {  
   private final Logger log = LoggerFactory.getLogger(this.getClass());  
   @Override  
   public List<PriceHistory> getPriceHistories() {  
     List<PriceHistory> histories = new ArrayList<>();  
     log.debug("Scanning For all PriceHistory items");  
     ItemCollection<ScanOutcome> scanResults = getHistoryTable().scan();  
     for (Item item : scanResults) {  
       log.debug("Item {} from results returned in results", item);  
       PriceHistory priceHistory = PriceHistoryMapper.mapToPriceHistory(item);  
       if (priceHistory != null) {  
         histories.add(priceHistory);  
       }  
     }  
     log.debug("Returning {} price histories", histories.size());  
     return histories;  
   }  
   @Override  
   public PriceHistory getPriceHistory(String searchUuid) {  
     log.debug("Attempting to retrieve PriceHistory with ID : {}", searchUuid);  
     Item persistedHistory = getHistoryTable().getItem(PriceHistoryMapper.PRIMARY_KEY_NAME, searchUuid);  
     if (persistedHistory != null) {  
       log.debug("History found in DynamoDB. {}", persistedHistory.toJSONPretty());  
       return PriceHistoryMapper.mapToPriceHistory(persistedHistory);  
     }  
     return new PriceHistory();  
   }  
   @Override  
   public void storePrice(PriceHistory priceHistory) {  
     log.debug("Storing history with ID : {}", priceHistory.getSearchUUID());  
     Item item = PriceHistoryMapper.mapFromPriceHistory(priceHistory);  
     PutItemOutcome putItemOutcome = getHistoryTable().putItem(item);  
     log.debug("Create response : {}", putItemOutcome);  
   }  
   private Table getHistoryTable() {  
     log.debug("Creating Table API object for table : {}", PriceHistoryMapper.TABLE_NAME);  
     DynamoDB dynamoDB = getDynamoDB();  
     return dynamoDB.getTable(PriceHistoryMapper.TABLE_NAME);  
   }  
   private DynamoDB getDynamoDB() {  
     log.debug("Creating AmazonDynamoDB client");  
     AmazonDynamoDB client = AmazonDynamoDBClientBuilder.defaultClient();  
     log.debug("AmazonDynamoDB client : {}", client);  
     return new DynamoDB(client);  
   }  
 }  


Got the following error when running the function in AWS ...

 User: arn:aws:sts::675239832622:assumed-role/holidayCheckRole/holidayPriceCheck is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:eu-west-2:675239832622:table/PriceHistory (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: AccessDeniedException; Request ID: K8T3CA4G8RMTBINN546UDMI5RFVV4KQNSO5AEMVJF66Q9ASUAAJG): com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException  
 com.amazonaws.services.dynamodbv2.model.AmazonDynamoDBException: User: arn:aws:sts::675239832622:assumed-role/holidayCheckRole/holidayPriceCheck is not authorized to perform: dynamodb:GetItem on resource: arn:aws:dynamodb:eu-west-2:675239832622:table/PriceHistory (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: AccessDeniedException; Request ID: K8T3CA4G8RMTBINN546UDMI5RFVV4KQNSO5AEMVJF66Q9ASUAAJG)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.handleErrorResponse(AmazonHttpClient.java:1695)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeOneRequest(AmazonHttpClient.java:1350)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeHelper(AmazonHttpClient.java:1101)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.doExecute(AmazonHttpClient.java:758)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.executeWithTimer(AmazonHttpClient.java:732)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.execute(AmazonHttpClient.java:714)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutor.access$500(AmazonHttpClient.java:674)  
     at com.amazonaws.http.AmazonHttpClient$RequestExecutionBuilderImpl.execute(AmazonHttpClient.java:656)  
     at com.amazonaws.http.AmazonHttpClient.execute(AmazonHttpClient.java:520)  
     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.doInvoke(AmazonDynamoDBClient.java:4192)  
     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.invoke(AmazonDynamoDBClient.java:4159)  
     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.executeGetItem(AmazonDynamoDBClient.java:2028)  
     at com.amazonaws.services.dynamodbv2.AmazonDynamoDBClient.getItem(AmazonDynamoDBClient.java:1995)  
     at com.amazonaws.services.dynamodbv2.document.internal.GetItemImpl.doLoadItem(GetItemImpl.java:77)  
     at com.amazonaws.services.dynamodbv2.document.internal.GetItemImpl.getItemOutcome(GetItemImpl.java:40)  
     at com.amazonaws.services.dynamodbv2.document.internal.GetItemImpl.getItemOutcome(GetItemImpl.java:99)  
     at com.amazonaws.services.dynamodbv2.document.internal.GetItemImpl.getItem(GetItemImpl.java:111)  
     at com.amazonaws.services.dynamodbv2.document.Table.getItem(Table.java:624)  
     at com.daledev.holidaypricescrapper.dao.aws.DynamoDBPriceStoreDao.getPriceHistory(DynamoDBPriceStoreDao.java:28)  
     at com.daledev.holidaypricescrapper.service.PriceRetrieverServiceImpl.priceCheck(PriceRetrieverServiceImpl.java:82)  
     at com.daledev.holidaypricescrapper.function.PriceCheckFunction.apply(PriceCheckFunction.java:30)  
     at com.daledev.holidaypricescrapper.function.PriceCheckFunction.apply(PriceCheckFunction.java:15)  
     at org.springframework.cloud.function.core.FluxFunction.lambda$apply$0(FluxFunction.java:47)  
     at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:107)  
     at reactor.core.publisher.FluxJust$WeakScalarSubscription.request(FluxJust.java:99)  
     at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:156)  
     at reactor.core.publisher.BlockingIterable$SubscriberIterator.onSubscribe(BlockingIterable.java:214)  
     at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onSubscribe(FluxMapFuseable.java:90)  
     at reactor.core.publisher.FluxJust.subscribe(FluxJust.java:70)  
     at reactor.core.publisher.FluxMapFuseable.subscribe(FluxMapFuseable.java:63)  
     at reactor.core.publisher.Flux.subscribe(Flux.java:6877)  
     at reactor.core.publisher.BlockingIterable.iterator(BlockingIterable.java:80)  
     at org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler.result(SpringBootRequestHandler.java:54)  
     at org.springframework.cloud.function.adapter.aws.SpringBootRequestHandler.handleRequest(SpringBootRequestHandler.java:49)  

The lambda function required the role is assigned to have AmazonDynamoDBFullAccess granted, I added this in the AWS IAM (Identity and Access Management) area.


Persist Search Criteria

I modified the JSON document to also hold the search criteria as well as the history of price changes, example content...

 {  
  "searchUUID" : "495f1431-5526-4083-997d-f91849be966b",  
  "criterion" : {  
   "description" : "Holiday Village in KOS 1 week in May-June 2019",  
   "startDate" : "2019-05-01T00:00:00.000+0000",  
   "endDate" : "2019-06-30T00:00:00.000+0000",  
   "airports" : [ {  
    "code" : "STN",  
    "uniqueId" : null  
   } ],  
   "duration" : 7,  
   "accommodationRef" : "047406:HOTEL",  
   "numberOfAdults" : 2,  
   "childrenAges" : [ 2 ]  
  },  
  "history" : [ {  
   "firstRetrievedDate" : "2019-01-06T21:35:59.700+0000",  
   "lastRetrievedDate" : "2019-01-06T21:53:00.319+0000",  
   "cheapestQuotes" : [ {  
    "airport" : {  
     "code" : "STN",  
     "uniqueId" : null  
    },  
    "date" : "2019-05-08T00:00:00.000+0000",  
    "price" : 613.0  
   } ]  
  }, {  
   "firstRetrievedDate" : "2019-01-07T14:16:22.610+0000",  
   "lastRetrievedDate" : "2019-01-07T17:47:00.076+0000",  
   "cheapestQuotes" : [ {  
    "airport" : {  
     "code" : "STN",  
     "uniqueId" : null  
    },  
    "date" : "2019-05-08T00:00:00.000+0000",  
    "price" : 611.0  
   } ]  
  }, {  
   "firstRetrievedDate" : "2019-01-08T12:34:23.896+0000",  
   "lastRetrievedDate" : "2019-01-08T12:34:23.896+0000",  
   "cheapestQuotes" : [ {  
    "airport" : {  
     "code" : "STN",  
     "uniqueId" : null  
    },  
    "date" : "2019-05-07T23:00:00.000+0000",  
    "price" : 613.0  
   } ]  
  } ],  
  "subscribers" : [ "dale.ellis@netcall.com", "daleellis1983@gmail.com" ],  
  "active" : true  
 }  


Other Changes


I did a pretty major refactor of my project, I split my code into a multi-module project, the issue I was having was that I was bundling the controller code for running the app locally in the AWS build, when running locally, the way I had it setup wouldn't compile as there were AWS dependencies that where mandatory which weren't included in a local build so I was getting errors.

New structure...




Module NameDescription
holiday-price-scraper-awsDynamoDB implementation for persistence, Spring cloud function adapter for AWS.
holiday-price-scraper-bootMain application, business logic
holiday-price-scraper-local-restFile system implementation for persistence, REST API to test locally
holiday-price-scraper-persistenceContains domain objects and DAO interfaces

Source Code



What's Next in Part 3


The desire is to be able to deploy this to other FaaS providers, but before I tackle Azure, I would like to tidy up some loose ends first...
  • AWS fails to send emails via Gmail, I have allowed insecure apps in Gmail enabled and still email fails to send, will setup sending emails using SES.
  • Implement email templates to use when sending an email with all the data that I would like to see.
  • Handle when the holiday is no longer available.
  • Complete README.md file

Sunday, 14 October 2018

Graph CRM - proof of concept


Introduction

Having worked on a CRM project in the past, there were things the product did well and things the product didn’t do so well or didn’t do at all. What I would like to do is take what I have done over the years and use that knowledge to build more complete product.

CRM Experience

Originally marketed as a CRM system, the product I worked on was more of a case management system and data integration view. The product has a rudimentary BPM engine to drive cases, it also has a integration piece also acting as a rudimentary BPM engine. The product had dashboards displaying data gathered from multiple sources, these 2 things were the key selling points of CRM. Other notable features of CRM, it also has a forms engine which allows for the designing of forms that can be used in the core application, a portal or mobile application.
CRM Systems


A typical system would usually consist of the following features (
Taken from https://www.salesforce.com/eu/learning-centre/crm/crm-systems/ and https://crm.walkme.com/components-customer-relationship-management/
)

Contact management

All the latest information about customers — from contact details to service conversations — is easily available to access and update.

Customer Service

Customer Relationship Management emphasises on collecting customer information and data, their purchase information and patterns as well as involves providing the collected information to the necessary and concerned departments. This makes customer service an essential component of CRM. Almost all the major departments including the sales department, marketing team and the management personnel are required to take steps to develop their awareness and understanding of the customer needs as well as complaints. This undoubtedly makes the business or the company to deliver quick and perfect solutions and assistance to the customers as well as cater to their needs which increases the dependability and trust of the customers and people on the organisation.

Lead management

Lead Management as the name suggests, refers to keeping the track of the sales leads as well as their distribution. The business that are benefited by this component of CRM the most are the sales industries, marketing firms and customer executive centres. It involves an efficient management of the campaigns, designing customised forms, finalising the mailing lists and several other elements. An extensive study of the purchase patterns of the customers as well as potential sales leads helps to capture the maximum number of sales leads to improve the sales.

HR Management

Human Resource Management involves the effective and correct use of human resource and skills at the specific moment and situation. This requires to be make sure that the skills and intellectual levels of the professionals match the tasks undertaken by them according to their job profiles. It is an essential component not only for the large scale corporations but the medium industries as well. It involves adopting an effective people strategy and studying the skills or the workforce and the growth being generated thereby designing and implementing the strategies needed accordingly with the aim of achieving development.

Sales forecasting

Forecasting reports enable salespeople to get better visibility over their pipelines, qualify leads more accurately, and see how close they are to hitting their targets. Sales managers can use reports to motivate and manage their people.

Instant messaging between employees

Real-time instant messaging functionality makes it easier for coworkers to ask and answer each other’s queries, for instance in support of a live sales opp or service interaction. Managers can check in on staff in the field, and employees can ask for instant feedback or support as needed.

Email tracking and integration with Outlook and Gmail

Syncing email clients instantly with the CRM system allows business people to get a complete view of their customers and leads without having to log in and out of different systems. Calendars and contacts can be viewed across every device, and emails can be created and managed from within a single workflow.

Marketing

Marketing is one of the most significant component of Customer Relationship Management and it refers to the promotional activities that are adopted by a company in order to promote their products. The marketing could be targeted to a particular group of people as well as to the general crowd. Marketing involves crafting and implementing strategies in order to sell the product. Customer Relationship Management assists in the marketing process by enhancing and improving the effectiveness of the strategies used for marketing and promotion. This is done by making an observation and study of the potential customers. It is a component that brings along various sub-elements or aspects. Some of the major elements of marketing are List Management, Campaign Management, Activity Management, Document Management, Call Management, Mass Emails and Reporting. The use of the aforesaid elements varies from business to business according to its nature and requirements as well as the target crowd.

Workflow Automation

A number of processes run simultaneously when it comes to the management and this requires an efficient cost cutting as well as the streamlining of all the processes.The phenomenon of doing so is known as Workflow Automation. It not only reduces the excess expenditure but also prevents the repetition of a particular task by different people by reducing the work and work force that is getting wasted for avoidable jobs. Routing out the paperwork and form filling are some of the elements of the process and it aims at preventing the loss of time and excess effort.

File and content sharing

Team members can upload information to a centrally stored location, and share easily and instantly with coworkers.

Dashboard-based analytics

Information is aggregated and presented in intuitive, meaningful dashboard displays that can be customised based on each individual’s priorities.

Graph Database

This project is born initially out of the desire for me to play around with graph databases and primarily the one I'm focusing on is NEO4J. If nothing else comes from this project it will give me the change to play around with NEO4J. The previous CRM product I work on had and issue with it's contact system, it was limited and adding relationships between different types of contacts was not easy, querying which join between different types of contact in most cases was very slow and in many cases was not possible at all. I've always felt a graph database sounds from the little I know of them to be a good good fit to easily add that capability so this project will allow me to find out if that is the case. From the table above, I believe a flexible entity system would allow for the handling of all that different types of data.

Plan

I have a very rough plan in mind for a proof of concept, I want to use NEO4J to build an entity system. Where before in my old CRM system, I spent a lot of effort in writing the case management functionality, this case management software was no where near being a full BPMN engine, the thought I had is that there most be open source BPMN engines that I could utilise to support case management, and after a small amount of time and watching videos on a particular product I found Apache Activiti. So the plan at the moment is simple, take Activiti, create an entity system with NEO4J integrate the 2 together into a CRM system.

Roadmap

  1. Create a entity system in NEO4J which allows me to define entities, I should be able to define from data the entity holds and how different types of entity can relate. 
  2. Implement entity searching to perform cross entity searching 
  3. Create basic GUI to support the entity creation and search features. 
  4. Setup Activiti 
  5. Implement user service and integrate with the CRM app and Activiti instance 
  6. Push cases into CRM system and display in GUI.