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