Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
Cache.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include "Stroika/Foundation/Cache/LRUCache.h"
9#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
10#include "Stroika/Foundation/IO/Network/HTTP/Methods.h"
13
14#include "Cache.h"
15
16using namespace Stroika::Foundation;
17using namespace Stroika::Foundation::Cache;
20using namespace Stroika::Foundation::IO;
23using namespace Stroika::Foundation::Time;
24
25// Comment this in to turn on aggressive noisy DbgTrace in this module
26// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
27
28namespace {
29
30 struct DefaultCacheRep_ : Transfer::Cache::IRep {
31
32 using Element = Transfer::Cache::Element;
33 using EvalContext = Transfer::Cache::EvalContext;
34 using DefaultOptions = Transfer::Cache::DefaultOptions;
35
36 struct MyElement_ : Element {
37 MyElement_ () = default;
38 MyElement_ (const Response& response)
39 : Element{response}
40 {
41 }
42
43 virtual optional<DateTime> IsValidUntil () const override
44 {
45 if (auto tmp = Element::IsValidUntil ()) {
46 return *tmp;
47 }
48 if (fExpiresDefault) {
49 return *fExpiresDefault;
50 }
51 return nullopt;
52 }
53
54 nonvirtual String ToString () const
55 {
57 sb << ", ExpiresDefault: "sv << fExpiresDefault;
58 sb << "}"sv;
59 return sb;
60 }
61 optional<Time::DateTime> fExpiresDefault;
62 };
63
64 DefaultCacheRep_ (const DefaultOptions& options)
65 : fOptions_{options}
66 , fCache_{options.fCacheSize.value_or (101), 11u}
67 {
68 }
69
70 virtual optional<Response> OnBeforeFetch (EvalContext* context, const URI& schemeAndAuthority, Request* request) noexcept override
71 {
72#if USE_NOISY_TRACE_IN_THIS_MODULE_
74 "IO::Network::Transfer ... {}::DefaultCacheRep_::OnBeforeFetch", "schemeAndAuthority={}"_f, schemeAndAuthority)};
75#endif
76 if (request->fMethod == HTTP::Methods::kGet) {
77 try {
78 URI fullURI = schemeAndAuthority.Combine (request->fAuthorityRelativeURL);
79 context->fFullURI = fullURI; // only try to cache GETs (for now)
80 if (optional<MyElement_> o = fCache_.Lookup (fullURI)) {
81 // check if cacheable - and either return directly or
82 DateTime now = DateTime::Now ();
83 bool canReturnDirectly = o->IsValidUntil ().value_or (now) > now;
84 if (canReturnDirectly) {
85 // fill in response and return short-circuiting normal full web fetch
86 Mapping<String, String> headers = o->GetCombinedHeaders ();
87 if (fOptions_.fCachedResultHeader) {
88 headers.Add (*fOptions_.fCachedResultHeader, String{});
89 }
90 return Response{o->fBody, HTTP::StatusCodes::kOK, headers};
91 }
92 // @todo - UNCLEAR if we should always include both etag/iflastmodiifeid if both available? Also may want to consider MAXAGE etc.
93 // AND maybe fOptions control over how we do this?
94 // -- LGP 2019-07-10
95 bool canCheckCacheETAG = o->fETag.has_value ();
96 if (canCheckCacheETAG) {
97 context->fCachedElement = *o;
98 request->fOverrideHeaders.Add (HTTP::HeaderName::kIfNoneMatch, "\""sv + *o->fETag + "\""sv);
99 // keep going as we can combine If-None-Match/If-Modified-Since
100 }
101 bool canCheckModifiedSince = o->fLastModified.has_value ();
102 if (canCheckModifiedSince) {
103 context->fCachedElement = *o;
104 request->fOverrideHeaders.Add (HTTP::HeaderName::kIfModifiedSince,
105 "\""sv + o->fLastModified->Format (DateTime::kRFC1123Format) + "\""sv);
106 }
107 }
108 }
109 catch (...) {
110 DbgTrace ("Cache::OnBeforeFetch::oops: {}"_f, current_exception ()); // ignore...
111 }
112 }
113 // In this case, no caching is possible - nothing todo
114 return nullopt;
115 }
116
117 virtual void OnAfterFetch (const EvalContext& context, Response* response) noexcept override
118 {
119#if USE_NOISY_TRACE_IN_THIS_MODULE_
120 Debug::TraceContextBumper ctx{"DefaultCacheRep_::OnAfterFetch", "context.fFullURI={}"_f, context.fFullURI};
121#endif
122 RequireNotNull (response);
123 switch (response->GetStatus ()) {
124 case HTTP::StatusCodes::kOK: {
125 if (context.fFullURI) {
126 try {
127 MyElement_ cacheElement{*response};
128 if (fOptions_.fDefaultResourceTTL) {
129 cacheElement.fExpiresDefault = DateTime::Now () + *fOptions_.fDefaultResourceTTL;
130 }
131 if (cacheElement.IsCachable ()) {
132 //DbgTrace ("Add2Cache: uri={}, cacheElement={}"_f, *context.fFullURI, cacheElement);
133 fCache_.Add (*context.fFullURI, cacheElement);
134 }
135 }
136 catch (...) {
137 DbgTrace ("Cache::OnAfterFetch::oops(ok): {}"_f, current_exception ()); // ignore...
138 }
139 }
140 } break;
141 case HTTP::StatusCodes::kNotModified: {
142 try {
143 /*
144 * Not 100% clear what to return here as status. Mostly want 200. But also want to know - sometimes - that the result
145 * was from cache. For now - return 200, but mark it with header fOptions_.fCachedResultHeader
146 * -- LGP 2019-06-27
147 */
148 if (context.fCachedElement) {
149 Mapping<String, String> headers = context.fCachedElement->GetCombinedHeaders ();
150 if (fOptions_.fCachedResultHeader) {
151 headers.Add (*fOptions_.fCachedResultHeader, String{});
152 }
153 *response = Response{context.fCachedElement->fBody, HTTP::StatusCodes::kOK, headers, response->GetSSLResultInfo ()};
154 }
155 else {
156 DbgTrace ("Cache::OnAfterFetch::oops: unexpected NOT-MODIFIED result when nothing was in the cache"_f); // ignore...
157 }
158 }
159 catch (...) {
160 DbgTrace ("Cache::OnAfterFetch::oops(ok): {}"_f, current_exception ()); // ignore...
161 }
162 } break;
163 default: {
164 // ignored
165 } break;
166 }
167 }
168
169 virtual void ClearCache () override
170 {
171 fCache_.clear ();
172 }
173
174 virtual optional<Element> Lookup (const URI& url) const override
175 {
176 return fCache_.Lookup (url);
177 }
178
179 DefaultOptions fOptions_;
180
181 using TRAITS_ =
183 mutable LRUCache<URI, MyElement_, TRAITS_> fCache_; // lookups DO mutate the cache so mark mutable, but safe threadwise cuz of InternallySynchronizedTraits
184 };
185
186}
187
188/*
189 ********************************************************************************
190 ******************* Transfer::Cache::Element ***********************************
191 ********************************************************************************
192 */
193Transfer::Cache::Element::Element (const Response& response)
194 : fBody{response.GetData ()}
195{
196 Mapping<String, String> headers = response.GetHeaders ();
197 for (auto hi = headers.begin (); hi != headers.end ();) {
198 // HTTP Date formats:
199 //
200 // According to https://tools.ietf.org/html/rfc7234#section-5.3
201 // The Expires value is an HTTP-date timestamp, as defined in Section 7.1.1.1 of[RFC7231].
202 // From https://tools.ietf.org/html/rfc7231#section-7.1.1.1
203 // The preferred format is
204 // a fixed - length and single - zone subset of the date and time specification used by the Internet Message Format[RFC5322].
205 //
206 if (hi->fKey == HTTP::HeaderName::kETag) {
207 if (hi->fValue.size () < 2 or not hi->fValue.StartsWith ("\""sv) or not hi->fValue.EndsWith ("\""sv)) {
208 Execution::Throw (Execution::Exception{"malformed etag"sv});
209 }
210 fETag = hi->fValue.SubString (1, -1);
211 hi = headers.erase (hi);
212 }
213 else if (hi->fKey == HTTP::HeaderName::kExpires) {
214 try {
215 fExpires = DateTime::Parse (hi->fValue, DateTime::kRFC1123Format);
216 }
217 catch (...) {
218 // treat invalid dates as if the resource has already expired
219 //fExpires = DateTime::min (); // better but cannot convert back to date - fix stk date stuff so this works
220 fExpires = DateTime::Now ();
221 DbgTrace ("Malformed expires ({}) treated as expires immediately"_f, hi->fValue);
222 }
223 hi = headers.erase (hi);
224 }
225 else if (hi->fKey == HTTP::HeaderName::kLastModified) {
226 try {
227 fLastModified = DateTime::Parse (hi->fValue, DateTime::kRFC1123Format);
228 }
229 catch (...) {
230 DbgTrace ("Malformed last-modified ({}) treated as ignored"_f, hi->fValue);
231 }
232 hi = headers.erase (hi);
233 }
234 else if (hi->fKey == HTTP::HeaderName::kCacheControl) {
235 fCacheControl = Set<String>{hi->fValue.Tokenize ({','})};
236 hi = headers.erase (hi);
237 static const String kMaxAgeEquals_{"max-age="sv};
238 for (const String& cci : *fCacheControl) {
239 if (cci.StartsWith (kMaxAgeEquals_)) {
240 fExpiresDueToMaxAge = DateTime::Now () + Duration{FloatConversion::ToFloat (cci.SubString (kMaxAgeEquals_.size ()))};
241 }
242 }
243 }
244 else if (hi->fKey == HTTP::HeaderName::kContentType) {
245 fContentType = DataExchange::InternetMediaType{hi->fValue};
246 hi = headers.erase (hi);
247 }
248 else {
249 ++hi;
250 }
251 }
252 fOtherHeaders = headers;
253}
254
255Mapping<String, String> Transfer::Cache::Element::GetCombinedHeaders () const
256{
257 Mapping<String, String> result = fOtherHeaders;
258 if (fETag) {
259 result.Add (HTTP::HeaderName::kETag, "\""sv + *fETag + "\""sv);
260 }
261 if (fExpires) {
262 result.Add (HTTP::HeaderName::kExpires, fExpires->Format (DateTime::kRFC1123Format));
263 }
264 if (fLastModified) {
265 result.Add (HTTP::HeaderName::kLastModified, fLastModified->Format (DateTime::kRFC1123Format));
266 }
267 if (fCacheControl) {
268 function<String (const String& lhs, const String& rhs)> a = [] (const String& lhs, const String& rhs) -> String {
269 return lhs.empty () ? rhs : (lhs + ","sv + rhs);
270 };
271 result.Add (HTTP::HeaderName::kCacheControl, fCacheControl->Reduce (a).value_or (String{}));
272 }
273 if (fContentType) {
274 result.Add (HTTP::HeaderName::kContentType, fContentType->As<String> ());
275 }
276 return result;
277}
278
280{
281 static const String kNoStore_{"no-store"sv};
282 if (fCacheControl) {
283 return not fCacheControl->Contains (kNoStore_);
284 }
285 return true;
286}
287
288optional<DateTime> Transfer::Cache::Element::IsValidUntil () const
289{
290 if (fExpires) {
291 return *fExpires;
292 }
293 if (fExpiresDueToMaxAge) {
294 return *fExpiresDueToMaxAge;
295 }
296 static const String kNoCache_{"no-cache"sv};
297 if (fCacheControl and fCacheControl->Contains (kNoCache_)) {
298 return DateTime::Now ().AddSeconds (-1);
299 }
300 return nullopt;
301}
302
304{
305 StringBuilder sb;
306 sb << "{"sv;
307 sb << ", ETag: "sv << fETag;
308 sb << ", Expires: "sv << fExpires;
309 sb << ", ExpiresDueToMaxAge: "sv << fExpiresDueToMaxAge;
310 sb << ", LastModified: "sv << fLastModified;
311 sb << ", CacheControl: "sv << fCacheControl;
312 sb << ", ContentType: "sv << fContentType;
313 sb << ", OtherHeaders: "sv << fOtherHeaders;
314 sb << ", Body: "sv << fBody;
315 sb << "}"sv;
316 return sb;
317}
318
319/*
320 ********************************************************************************
321 **************************** Transfer::Cache ***********************************
322 ********************************************************************************
323 */
324Transfer::Cache::Ptr Transfer::Cache::CreateDefault ()
325{
326 return CreateDefault (DefaultOptions{});
327}
328Transfer::Cache::Ptr Transfer::Cache::CreateDefault (const DefaultOptions& options)
329{
330 return Ptr{Memory::MakeSharedPtr<DefaultCacheRep_> (options)};
331}
#define RequireNotNull(p)
Definition Assertions.h:348
#define DbgTrace
Definition Trace.h:317
#define Stroika_Foundation_Debug_OptionalizeTraceArgs(...)
Definition Trace.h:278
LRUCache implements a simple least-recently-used caching strategy, with optional hashing (of keys) to...
Definition LRUCache.h:224
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
nonvirtual bool Contains(Character c, CompareOptions co=eWithCase) const
Definition String.inl:697
nonvirtual String SubString(SZ from) const
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:188
nonvirtual void erase(ArgByValueType< key_type > key)
Definition Mapping.inl:468
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
Exception<> is a replacement (subclass) for any std c++ exception class (e.g. the default 'std::excep...
Definition Exceptions.h:157
nonvirtual URI Combine(const URI &overridingURI) const
Combine overridingURI possibly relative url with this base url, to produce a new URI.
Definition URI.cpp:316
Duration is a chrono::duration<double> (=.
Definition Duration.h:96
nonvirtual Iterator< T > begin() const
Support for ranged for, and STL syntax in general.
static constexpr default_sentinel_t end() noexcept
Support for ranged for, and STL syntax in general.
String ToString(T &&t, ARGS... args)
Return a debug-friendly, display version of the argument: not guaranteed parsable or usable except fo...
Definition ToString.inl:465
InternallySynchronizedTraits same as argument traits, but resetting the kInternallySynchronized to eI...
Definition LRUCache.h:136
virtual optional< Time::DateTime > IsValidUntil() const
Definition Cache.cpp:288