Friday, October 16, 2020

GraphQL Upload: Pitfalls & Tricks

1. Fileupload is a big deal for GraphQL in general. As of today `apollo-server-lambda` does not support fileupload yet. The work-around is to use some server or middleware process to achieve it.

2. GraphQL documentation for Upload is limited

3. Express/Apollo or any other server with AWS Lambda is needed for GraphQL FileStreams handling

4. AWS API Gateway needed to be configured with Binary Media Types. References:

  • https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-payload-encodings.html
  • https://stackoverflow.com/questions/41756190/api-gateway-post-multipart-form-data

5. Binary input is expected by all GraphQL mutations when binary media types is turned on

6. CORS can be more challenging to handle at the API Gateway level due to the single GraphQL end point

7. Limited information/support on streaming libraries viz. Busboy, Form-Data, Formidable, Multer, Multiparty. Finally decided to use FormData by properly passing mimetype,filename,encoding. Reference:

  • https://npmcompare.com/compare/busboy,form-data,formidable,multer,multiparty

8. Stream hand-over had challenges. There are libraries (mentioned in above point) that help with parsing the file and text from the incoming request and handing it over to another API. References:

  • https://stackoverflow.com/questions/52963648/how-to-pass-multipart-request-from-one-server-to-another-in-nodejs
  • https://github.com/apollographql/apollo-server/issues/1854

9. Async/Await for FileStreams seems to be misbehaving in with AWS Lambda environment causing File size becomes 0 while API responding with 200. Reference:

  • https://levelup.gitconnected.com/avoiding-the-pitfalls-of-async-node-js-functions-in-aws-lambda-941220582e7a

GraphQL vs REST?

- GraphQL REST
Architecture Client-driven Server-driven
Organized in terms of Schema & type system Endpoints
Operations Query
Mutation
Subscription
Create
Read
Update
Write
Data fetching Specific data with single API call Fixed data with multiple API calls
Community Growing Large
Performance Fast Multiple network calls take up more time
Development Speed Rapid Slower
Learning Curve Difficult Moderate
Self-documenting Using introspection. -
File uploading Very challenging -
Web caching via libraries built on top -
Stability Lesser error prone: automatic validation and type checking Better choice for complex queries
Use cases Multiple micro-services
Aggregators
Mobile Apps
Simple apps
Resource driven apps

GraphQL Introduction

After being publicly released by Facebook in 2015 GraphQL Specification has seen a staggering adoption in the software industry. Below are some facts about it:

  • GraphQL is an open-source data query and manipulation language for APIs, and a runtime for fulfilling queries with existing data. GraphQL was introduced internally at Facebook in 2012 to address the issue with RESTful APIs around over-fetching/under-fetching of data for their mobile application.
  • GraphQL Spec has many implementations which open-source-community created. This was after first RecatJS implementation was created by Facebook. Implementations are present for languages such as: Javascript/Typescript, Go, Ruby, Scala, Elixir, Python, Java. Few popular implementations are Apollo, AWS AppSync, Prisma, Hasura, etc. With this increasing adoption, GraphQL Tooling & Ecosystem has also seen a huge surge.
  • GraphQL has 3 request types: queries (for getting data from the API), mutations (for changing data via the API), and subscriptions (long-lived connections for streaming data from the API).
  • GraphQL is Typed. Contrary to REST; the GraphQL does runtime datatype checking & sub-selection to limit fields returned.
  • GraphQL schemas are composed of Resolvers. That means data can be pulled from anywhere/any source-system independently. A resolver is a function that resolves a value for a type or field in a schema. Resolvers can be asynchronous too! A resolver function takes four arguments (in that order):
    • 1. parent: The result of the previous resolver call (more info).
    • 2. args: The arguments of the resolver’s field.
    • 3. context: A custom object each resolver can read from/write to.
    • 4. info: Contains the query AST and more execution information. In other words it contains, Meta-data about the request.
  • GraphQL can return multiple resources in one round trip to server.
  • GraphQL ensures no over-fetching as well as no under-fetching. API Clients are in full control about what they want to query. If some client wants a single field to query or make an update then it can do so without need of additional API Endpoints or a new version of the same API.
  • GraphQL is Introspectable. GraphiQL Tool exploits exactly that to provide client generation, query generation, suggestions & documentation. Think of it as Java Reflection.
  • One of the reasons customers choose GraphQL is the power of Subscriptions. These are notifications that are sent immediately to clients when data has changed by a mutation. AppSync subscriptions are implemented using Websockets, and are directly tied to a mutation in the schema. The AppSync SDKs and AWS Amplify Library allow clients to subscribe to these real-time notifications. AppSync Subscriptions have many uses outside standard API CRUD operations. They can be used for inter-client communication, such as a mobile or web chat application. Subscription notifications can also be used to provide asynchronous responses to long-running requests. The initial request returns quickly, while the full result can be sent via subscription when it’s complete (local resolvers are useful for this pattern).
  • While there's nothing that prevents a GraphQL service from being versioned just like any other REST API, GraphQL takes a strong opinion on avoiding versioning by providing the tools for the continuous evolution of a GraphQL schema.

Thursday, August 15, 2019

Multi-tenancy Summarized

Multi-tenancy is an architecture in which a single instance of a software application serves multiple customers (tenants).

Consideration Points:

  1. Quantity of tenants
  2. Performance
  3. Time of development
  4. Reliability
  5. Disaster recovery
  6. Execution Environment Isolation: Adding, modifying or removing features for one customer should not impact other customers
  7. Features added for one customer may be applicable for other customers. Hence customization cannot be tied to a single customer. The architecture should allow same features to be enabled and disabled easily for other customers.
  8. Licenses can be upgraded or downgraded. Features should be grouped so that they can be easily added or removed from a customer execution environment.

Advantages:

  1. Deploy code without affecting other tenants in the environment
  2. Feature Customization: Enable features for a single tenant without disturbing other tenants
  3. Cost-efficient and Financial Benefits
  4. Efficient Resource Usage
  5. Easier Set Up and On-boarding
  6. Easier Upgrades and Maintenance

Disadvantages:

  1. Data Leakage and Reliability
  2. More control over backups and recovery
  3. Migration Ease of Use
  4. Full control over the environment
  5. More vulnerable from a security standpoint
  6. Single point of failure for all Tenants

Approaches:
  1. Separate databases: Each tenant has its own database. Eg. Maintain different databases for different tenants and connect to the correct database at runtime.
  2. Separate schemas: Tenants share common database but each tenant has its own set of tables (schema). Eg: Maintain a single database, but different schemas for different tenants qualified by the Company Id
  3. Shared schema aka Partitioned (discriminator) Data: Tenants share common schema and are distinguished by a tenant discriminator column. Eg: Add a Company Id to all the tables and qualify all queries with a company id.
  4. Data Sharding
  5. Hypervisor-level Isolation

Given the advantages and disadvantages of each approach, I'd recommend using separate schemas, as they provide the best balance between difficulty of development and cost of running the database layer. 

Friday, July 14, 2017

NoSQL Data Modelling

NoSQL Data Modelling has two choices viz. Embedding (non-normalized) vs Referencing (normalized).

Aspects to choose:
  • 1:1 relationship => Prefer Embedding
  • 1-to-Many (small & bounded) => Prefer Embedding
  • 1-to-Many (unbounded) => Prefer Referencing
  • Volatility (frequently changing sub-documents) => Prefer Referencing
  • Immense read-speeds needed => Prefer Embedding

Sunday, April 3, 2016

Summarized Hashing Fundamentals

Need of Hashing: Searching is typically the most frequent data operation. Linear Search on an unsorted array has O(n) while Binary Search on a sorted array has O(log n) time complexities. Hash Search further optimizes it to have a O(1) asymptotic time complexity.

Hash Function: A function that ensures uniform distribution of hash values & least collisions.

Collision: Collision happens when multiple keys hash to the same bucket.

Collision Avoidance Techniques:
  • Direct Chaining/Separate Chaining: Collided elements are added to Linked list.
  • Open Addressing: Involves Linear or Quadratic Probing.
    • Linear Probing: [( H(x) + f(i) ) mod ArrLen] preferred in simple scenarios.
    • Quadratic Probing: [( H(x) + f(i^2) ) mod ArrLen] preferred in simple scenarios.
  • Closed Addressing: Involves Double Hashing.
    • Double Hashing: [( H1(x) + (i * H2(x) ) ) mod ArrLen] where H2(x) = [PrimeNum – x mod PrimeNum] is for complex scenarios.
Collision Avoidance Tips:
  • Use separate bucket for null key (special case).
  • Avoid clustering of values one besides other.
  • Choose twice array size than the values to be inserted.
  • Choose Prime Number (smaller than table size) as the initial size of data-structure.
  • For double-hashing, ensure that second Hash function (H2(x)) shouldn't evaluate to zero & should probe all locations.
Collision Avoidance Example: Following example is the actual HashMap implementation code.

 public V put(K key, V value) {  
   if (key == null)  
     return putForNullKey(value); // Used separate bucket for null-key.  
   int hash = hash(key.hashCode()); // Called Hash-function to get hash-value.  
   int i = indexFor(hash, table.length);  
   for (Entry e = table[i]; e != null; e = e.next) {  
     Object k;  
     if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
       V oldValue = e.value;  
       e.value = value;  
       e.recordAccess(this);  
       return oldValue;  
     }  
   }  
   modCount++;  
   addEntry(hash, key, value, i); // Stored element using above hash-value.  
   return null;  
 }  

Saturday, March 26, 2016

Tips for approaching Algorithmic problems

  1. Exemplify (Investigation): Understand problem by creating minimum two examples of the algorithm. Use this step to create test-inputs.
  2. Algo Pattern: Identify the similar algorithms (or patterns of algorithms) that you've encountered previously. Use this step to identify best applicable data-structure. 
  3. Data Structure Brainstorm: This is hit and trial method. Try fitting relevant data structure till you find the best match. There are far better techniques for choosing it as well (watch this space and I shall update it shortly).
  4. Simplify and Generalize (Divide and Conquer): Break-down the big problem into smaller fragments. Use this step to modularize. 
  5. Base case and Build: Identify base case (eg. just one element case) for algorithm behavior and start from there. 
  6. Algo Features: Keep a check on sequence, decision, repetition and Asymptotic Complexity (Big O notation)