Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
Cache.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include "Stroika/Foundation/Cache/SynchronizedLRUCache.h"
9#include "Stroika/Foundation/IO/Network/HTTP/Headers.h"
10#include "Stroika/Foundation/IO/Network/HTTP/Methods.h"
12
13#include "Cache.h"
14
15using namespace Stroika::Foundation;
18using namespace Stroika::Foundation::IO;
21using namespace Stroika::Foundation::Time;
22
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
182 };
183
184}
185
186/*
187 ********************************************************************************
188 ******************* Transfer::Cache::Element ***********************************
189 ********************************************************************************
190 */
191Transfer::Cache::Element::Element (const Response& response)
192 : fBody{response.GetData ()}
193{
194 Mapping<String, String> headers = response.GetHeaders ();
195 for (auto hi = headers.begin (); hi != headers.end ();) {
196 // HTTP Date formats:
197 //
198 // According to https://tools.ietf.org/html/rfc7234#section-5.3
199 // The Expires value is an HTTP-date timestamp, as defined in Section 7.1.1.1 of[RFC7231].
200 // From https://tools.ietf.org/html/rfc7231#section-7.1.1.1
201 // The preferred format is
202 // a fixed - length and single - zone subset of the date and time specification used by the Internet Message Format[RFC5322].
203 //
204 if (hi->fKey == HTTP::HeaderName::kETag) {
205 if (hi->fValue.size () < 2 or not hi->fValue.StartsWith ("\""sv) or not hi->fValue.EndsWith ("\""sv)) {
206 Execution::Throw (Execution::Exception{"malformed etag"sv});
207 }
208 fETag = hi->fValue.SubString (1, -1);
209 hi = headers.erase (hi);
210 }
211 else if (hi->fKey == HTTP::HeaderName::kExpires) {
212 try {
213 fExpires = DateTime::Parse (hi->fValue, DateTime::kRFC1123Format);
214 }
215 catch (...) {
216 // treat invalid dates as if the resource has already expired
217 //fExpires = DateTime::min (); // better but cannot convert back to date - fix stk date stuff so this works
218 fExpires = DateTime::Now ();
219 DbgTrace ("Malformed expires ({}) treated as expires immediately"_f, hi->fValue);
220 }
221 hi = headers.erase (hi);
222 }
223 else if (hi->fKey == HTTP::HeaderName::kLastModified) {
224 try {
225 fLastModified = DateTime::Parse (hi->fValue, DateTime::kRFC1123Format);
226 }
227 catch (...) {
228 DbgTrace ("Malformed last-modified ({}) treated as ignored"_f, hi->fValue);
229 }
230 hi = headers.erase (hi);
231 }
232 else if (hi->fKey == HTTP::HeaderName::kCacheControl) {
233 fCacheControl = Set<String>{hi->fValue.Tokenize ({','})};
234 hi = headers.erase (hi);
235 static const String kMaxAgeEquals_{"max-age="sv};
236 for (const String& cci : *fCacheControl) {
237 if (cci.StartsWith (kMaxAgeEquals_)) {
238 fExpiresDueToMaxAge = DateTime::Now () + Duration{FloatConversion::ToFloat (cci.SubString (kMaxAgeEquals_.size ()))};
239 }
240 }
241 }
242 else if (hi->fKey == HTTP::HeaderName::kContentType) {
243 fContentType = DataExchange::InternetMediaType{hi->fValue};
244 hi = headers.erase (hi);
245 }
246 else {
247 ++hi;
248 }
249 }
250 fOtherHeaders = headers;
251}
252
253Mapping<String, String> Transfer::Cache::Element::GetCombinedHeaders () const
254{
255 Mapping<String, String> result = fOtherHeaders;
256 if (fETag) {
257 result.Add (HTTP::HeaderName::kETag, "\""sv + *fETag + "\""sv);
258 }
259 if (fExpires) {
260 result.Add (HTTP::HeaderName::kExpires, fExpires->Format (DateTime::kRFC1123Format));
261 }
262 if (fLastModified) {
263 result.Add (HTTP::HeaderName::kLastModified, fLastModified->Format (DateTime::kRFC1123Format));
264 }
265 if (fCacheControl) {
266 function<String (const String& lhs, const String& rhs)> a = [] (const String& lhs, const String& rhs) -> String {
267 return lhs.empty () ? rhs : (lhs + ","sv + rhs);
268 };
269 result.Add (HTTP::HeaderName::kCacheControl, fCacheControl->Reduce (a).value_or (String{}));
270 }
271 if (fContentType) {
272 result.Add (HTTP::HeaderName::kContentType, fContentType->As<String> ());
273 }
274 return result;
275}
276
278{
279 static const String kNoStore_{"no-store"sv};
280 if (fCacheControl) {
281 return not fCacheControl->Contains (kNoStore_);
282 }
283 return true;
284}
285
286optional<DateTime> Transfer::Cache::Element::IsValidUntil () const
287{
288 if (fExpires) {
289 return *fExpires;
290 }
291 if (fExpiresDueToMaxAge) {
292 return *fExpiresDueToMaxAge;
293 }
294 static const String kNoCache_{"no-cache"sv};
295 if (fCacheControl and fCacheControl->Contains (kNoCache_)) {
296 return DateTime::Now ().AddSeconds (-1);
297 }
298 return nullopt;
299}
300
302{
303 StringBuilder sb;
304 sb << "{"sv;
305 sb << ", ETag: "sv << fETag;
306 sb << ", Expires: "sv << fExpires;
307 sb << ", ExpiresDueToMaxAge: "sv << fExpiresDueToMaxAge;
308 sb << ", LastModified: "sv << fLastModified;
309 sb << ", CacheControl: "sv << fCacheControl;
310 sb << ", ContentType: "sv << fContentType;
311 sb << ", OtherHeaders: "sv << fOtherHeaders;
312 sb << ", Body: "sv << fBody;
313 sb << "}"sv;
314 return sb;
315}
316
317/*
318 ********************************************************************************
319 **************************** Transfer::Cache ***********************************
320 ********************************************************************************
321 */
322Transfer::Cache::Ptr Transfer::Cache::CreateDefault ()
323{
324 return CreateDefault (DefaultOptions{});
325}
326Transfer::Cache::Ptr Transfer::Cache::CreateDefault (const DefaultOptions& options)
327{
328 return Ptr{make_shared<DefaultCacheRep_> (options)};
329}
#define RequireNotNull(p)
Definition Assertions.h:347
#define DbgTrace
Definition Trace.h:309
#define Stroika_Foundation_Debug_OptionalizeTraceArgs(...)
Definition Trace.h:270
simple wrapper on LRUCache (with the same API) - but internally synchronized in a way that is more pe...
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:693
nonvirtual String SubString(SZ from) const
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:190
nonvirtual void erase(ArgByValueType< key_type > key)
Definition Mapping.inl:431
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
Definition Set.h:105
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
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
virtual optional< Time::DateTime > IsValidUntil() const
Definition Cache.cpp:286